├── .gitignore
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── src
├── utils
│ └── stream.ts
├── pathquickpick.ts
├── devicesprovider.ts
├── extension.ts
├── localize.ts
├── adb.ts
└── fsprovider.ts
├── .vscodeignore
├── .eslintrc.json
├── assets
└── adb.svg
├── tsconfig.json
├── package.nls.zh.json
├── README.md
├── LICENSE
├── package.nls.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 | package-lock.json
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/stream.ts:
--------------------------------------------------------------------------------
1 | import { Readable, Duplex } from 'stream';
2 |
3 | export function toReadable(data: Uint8Array): Readable {
4 | const stream = new Duplex();
5 | stream.push(data);
6 | stream.push(null);
7 | return stream;
8 | }
9 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | node_modules
4 | build/**
5 | out/test/**
6 | src/**
7 | .gitignore
8 | .yarnrc
9 | vsc-extension-quickstart.md
10 | **/tsconfig.json
11 | **/.eslintrc.json
12 | **/*.map
13 | **/*.ts
14 | package-lock.json
15 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/semi": "warn",
13 | "curly": "warn",
14 | "eqeqeq": "warn",
15 | "no-throw-literal": "warn",
16 | "semi": "off"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "webpack",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off"
11 | }
--------------------------------------------------------------------------------
/assets/adb.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/dist/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": [
7 | "es6"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true /* enable all strict type-checking options */
12 | /* Additional Checks */
13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
16 | },
17 | "exclude": [
18 | "node_modules",
19 | ".vscode-test",
20 | "dist",
21 | "out"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/package.nls.zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "gwo.android.extension.name": "Gwo 安卓助手",
3 | "gwo.android.extension.description": "通过 Visual Studio Code 在你的 Android 设备上浏览文件",
4 | "gwo.android.devices.viewTitle": "设备",
5 | "gwo.android.devices.refresh": "刷新",
6 | "gwo.android.devices.openInternalStorage": "打开内置存储",
7 | "gwo.android.devices.openFolder": "打开文件夹...",
8 | "gwo.android.devices.openTerminal": "在此设备上打开终端",
9 | "gwo.android.devices.welcome": "没有找到设备。",
10 | "gwo.android.devices.chooseFolderTitle": "在 {0} 上选择文件夹",
11 | "gwo.android.folder.openMessage": "\"{0}\" 已经在当前工作区打开了,切换到文件管理栏就可以看到了。",
12 | "gwo.android.folder.internalStorageTitle": "内置 - {0}",
13 | "gwo.android.terminal.title": "Adb 终端 - {0}",
14 | "gwo.android.type.device": "在线",
15 | "gwo.android.type.offline": "离线",
16 | "gwo.android.type.recovery": "恢复模式",
17 | "gwo.android.type.fastboot": "刷机模式",
18 | "gwo.android.type.sideload": "旁加载",
19 | "gwo.android.type.unauthorized": "未授权"
20 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Gwo Android Helper
2 | ======
3 |
4 | Browse files on your Android devices with Visual Studio Code
5 |
6 | ## Requirements
7 |
8 | - Visual Studio Code 1.53+
9 | - Android Debug Bridge (should be in `$PATH`)
10 |
11 | # What can it do?
12 |
13 | - Operate files on your Android devices in file manager tab just like your local files.
14 | - Open adb shell in terminal tab
15 |
16 | I develop this extension for managing my files between phone and computer. Without building a new program, using Visual Studio Code as a UI & file manager framework/platform saves a lot of time. So we didn't need to do much work for adapting project to different OS.
17 |
18 | ## Usage
19 |
20 | 1. Install extension from marketplace or compile by yourself: Gwo Android Helper
21 | 2. Click "Gwo Android Helper" tab in side bar
22 | 3. Find out your device and click "Open internal storage" (or choose other actions)
23 | 4. Switch back to "File explorer" tab and you can see device storage in current workspace
24 |
25 | ## License
26 |
27 | MIT License
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Siubeng (fython)
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 all
13 | 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 THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.nls.json:
--------------------------------------------------------------------------------
1 | {
2 | "gwo.android.extension.name": "Gwo Android Helper",
3 | "gwo.android.extension.description": "Browse files on your Android devices with Visual Studio Code",
4 | "gwo.android.devices.viewTitle": "Devices",
5 | "gwo.android.devices.refresh": "Refresh",
6 | "gwo.android.devices.openInternalStorage": "Open internal stroage",
7 | "gwo.android.devices.openFolder": "Open folder...",
8 | "gwo.android.devices.openTerminal": "Terminal on this device",
9 | "gwo.android.devices.welcome": "No devices found.",
10 | "gwo.android.devices.chooseFolderTitle": "Choose folder on {0}",
11 | "gwo.android.folder.openMessage": "\"{0}\" is open in current workspace. Switch to file explorer tab then you can see it.",
12 | "gwo.android.folder.internalStorageTitle": "Internal - {0}",
13 | "gwo.android.terminal.title": "Adb Shell - {0}",
14 | "gwo.android.type.device": "Online",
15 | "gwo.android.type.offline": "Offline",
16 | "gwo.android.type.recovery": "Recovery",
17 | "gwo.android.type.fastboot": "Fastboot",
18 | "gwo.android.type.sideload": "Sideload",
19 | "gwo.android.type.unauthorized": "Unauthorized"
20 | }
--------------------------------------------------------------------------------
/src/pathquickpick.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as adb from './adb';
3 | import { AdbDeviceTreeItem } from './devicesprovider';
4 | import localize from './localize';
5 |
6 | export function bind(box: vscode.QuickPick, item: AdbDeviceTreeItem, cb: (path: string) => void) {
7 | box.title = localize('gwo.android.devices.chooseFolderTitle', item.label as string);
8 | box.value = '/';
9 | box.canSelectMany = false;
10 |
11 | const loadAutoComplete = async (path: string) => {
12 | if (!path.startsWith('/')) {
13 | path = '/' + path;
14 | }
15 | box.busy = true;
16 | try {
17 | const dirs = (await adb.readDirectory(item.id, path))
18 | .filter(entry => !entry.isFile())
19 | .map(entry => path + (path.endsWith('/') ? '' : '/') + entry.name);
20 | box.items = [path, ...dirs].map(dir => new PathPickItem(dir));
21 | } finally {
22 | box.busy = false;
23 | }
24 | };
25 |
26 | box.onDidChangeValue(loadAutoComplete);
27 | box.onDidChangeSelection((entry: PathPickItem[]) => {
28 | if (entry.length === 1) {
29 | if (entry[0].label === box.value) {
30 | let targetPath = box.value;
31 | if (targetPath.endsWith('/') && targetPath !== '/') {
32 | targetPath = targetPath.substr(0, targetPath.length - 1);
33 | }
34 | cb(targetPath);
35 | box.dispose();
36 | }
37 | box.value = entry[0].label + '/';
38 | }
39 | loadAutoComplete(box.value);
40 | });
41 |
42 | loadAutoComplete(box.value);
43 | }
44 |
45 | class PathPickItem implements vscode.QuickPickItem {
46 | constructor(public label: string) {}
47 | }
48 |
--------------------------------------------------------------------------------
/src/devicesprovider.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { AdbDevice } from './adb';
3 | import * as adb from './adb';
4 | import localize from './localize';
5 |
6 | export class AdbDevicesProvider implements vscode.TreeDataProvider {
7 | private _onDidChangeTreeData: vscode.EventEmitter =
8 | new vscode.EventEmitter();
9 | readonly onDidChangeTreeData: vscode.Event =
10 | this._onDidChangeTreeData.event;
11 |
12 | constructor() {
13 | setInterval(() => {
14 | this.refresh();
15 | }, 1000 * 30);
16 | }
17 |
18 | refresh(): void {
19 | this._onDidChangeTreeData.fire();
20 | }
21 |
22 | getTreeItem(element: AdbDeviceTreeItem): vscode.TreeItem {
23 | return element;
24 | }
25 |
26 | async getChildren(element?: AdbDeviceTreeItem): Promise {
27 | if (element) {
28 | return [];
29 | } else {
30 | const devices = await adb.listDevices();
31 | return devices.map(device => new AdbDeviceTreeItem(device));
32 | }
33 | }
34 | }
35 |
36 | function getIconNameByDeviceType(type: string): string {
37 | switch (type) {
38 | case 'offline':
39 | return 'warning';
40 | default:
41 | return 'device-mobile';
42 | }
43 | }
44 |
45 | function getLocalizedDeviceType(type: string): string {
46 | const result = localize('gwo.android.type.' + type);
47 | if (result.indexOf('%') >= 0) {
48 | return type;
49 | }
50 | return result;
51 | }
52 |
53 | export class AdbDeviceTreeItem extends vscode.TreeItem {
54 | public readonly id: string;
55 | public type: string;
56 |
57 | constructor(device: AdbDevice) {
58 | super(device.id, vscode.TreeItemCollapsibleState.None);
59 | if (device.name?.length ?? 0 > 0) {
60 | this.label = `${device.name} (${device.id})`;
61 | }
62 | this.id = device.id;
63 | this.type = device.type;
64 | this.contextValue = 'adbDevice';
65 | this.description = getLocalizedDeviceType(device.type);
66 | this.tooltip = device.id;
67 | this.iconPath = new vscode.ThemeIcon(getIconNameByDeviceType(device.type));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { AdbDevicesProvider, AdbDeviceTreeItem } from './devicesprovider';
3 | import { AdbFileSystemProvider } from './fsprovider';
4 | import * as PathQuickPick from './pathquickpick';
5 | import localize from './localize';
6 |
7 | export function activate(context: vscode.ExtensionContext) {
8 | const devicesProvider = new AdbDevicesProvider();
9 | const adbFsProvider = new AdbFileSystemProvider();
10 | adbFsProvider.isDebugging = true;
11 |
12 | const refreshAdbDevices = () => devicesProvider.refresh();
13 | const openDeviceInternalStorage = (item: AdbDeviceTreeItem) => {
14 | console.log(`openDeviceInternalStorage: id=${item.id}`);
15 | vscode.workspace.updateWorkspaceFolders(0, null, {
16 | uri: vscode.Uri.parse(`adbfile:/${item.id}/internal`),
17 | name: localize('gwo.android.folder.internalStorageTitle', item.label as string),
18 | });
19 | vscode.window.showInformationMessage(
20 | localize('gwo.android.folder.openMessage', item.label as string));
21 | };
22 | const openDeviceFolder = (item: AdbDeviceTreeItem) => {
23 | console.log(`openDeviceFolder: id=${item.id}`);
24 | const quickPick = vscode.window.createQuickPick();
25 | PathQuickPick.bind(quickPick, item, (path: string) => {
26 | vscode.workspace.updateWorkspaceFolders(0, null, {
27 | uri: vscode.Uri.parse(`adbfile:/${item.id}/root${path}`),
28 | name: `${path} - ${item.label}`,
29 | });
30 | vscode.window.showInformationMessage(
31 | localize('gwo.android.folder.openMessage', `${item.label}:${path}`));
32 | });
33 | quickPick.show();
34 | };
35 | const openTerminal = (item: AdbDeviceTreeItem) => {
36 | console.log(`openTerminal: id=${item.id}`);
37 | const terminal = vscode.window.createTerminal(
38 | localize('gwo.android.terminal.title', item.id),
39 | 'adb', ['-s', item.id, 'shell']);
40 | terminal.show();
41 | };
42 |
43 | context.subscriptions.push(
44 | vscode.workspace.registerFileSystemProvider('adbfile', adbFsProvider, { isCaseSensitive: true }),
45 | vscode.window.registerTreeDataProvider('adbDevices', devicesProvider),
46 | vscode.commands.registerCommand('adbDevices.refresh', refreshAdbDevices),
47 | vscode.commands.registerCommand('adbDevices.openDeviceInternalStorage', openDeviceInternalStorage),
48 | vscode.commands.registerCommand('adbDevices.openDeviceFolder', openDeviceFolder),
49 | vscode.commands.registerCommand('adbDevices.openTerminal', openTerminal),
50 | );
51 | }
52 |
53 | export function deactivate() {}
54 |
--------------------------------------------------------------------------------
/src/localize.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync } from 'fs';
2 | import { resolve } from 'path';
3 | import { extensions } from 'vscode';
4 |
5 | const EXTENSION_ID = 'Siubeng.gwo-android-helper';
6 |
7 | export interface ILanguagePack {
8 | [key: string]: string;
9 | }
10 |
11 | export class Localize {
12 | private bundle = this.resolveLanguagePack();
13 | private options: { locale: string } = { locale: '' };
14 |
15 | public localize(key: string, ...args: string[]): string {
16 | const message = this.bundle[key] || key;
17 | return this.format(message, args);
18 | }
19 |
20 | private init() {
21 | try {
22 | this.options = {
23 | ...this.options,
24 | ...JSON.parse(process.env.VSCODE_NLS_CONFIG || '{}')
25 | };
26 | } catch (err) {
27 | throw err;
28 | }
29 | }
30 |
31 | private format(message: string, args: string[] = []): string {
32 | return args.length
33 | ? message.replace(
34 | /\{(\d+)\}/g,
35 | (match, rest: any[]) => args[rest[0]] || match
36 | )
37 | : message;
38 | }
39 |
40 | private resolveLanguagePack(): ILanguagePack {
41 | this.init();
42 |
43 | const languageFormat = 'package.nls{0}.json';
44 | const defaultLanguage = languageFormat.replace('{0}', '');
45 |
46 | const rootPath = extensions.getExtension(EXTENSION_ID)!
47 | .extensionPath;
48 |
49 | const resolvedLanguage = this.recurseCandidates(
50 | rootPath,
51 | languageFormat,
52 | this.options.locale
53 | );
54 |
55 | const languageFilePath = resolve(rootPath, resolvedLanguage);
56 |
57 | try {
58 | const defaultLanguageBundle = JSON.parse(
59 | resolvedLanguage !== defaultLanguage
60 | ? readFileSync(resolve(rootPath, defaultLanguage), 'utf-8')
61 | : '{}'
62 | );
63 |
64 | const resolvedLanguageBundle = JSON.parse(
65 | readFileSync(languageFilePath, 'utf-8')
66 | );
67 |
68 | return { ...defaultLanguageBundle, ...resolvedLanguageBundle };
69 | } catch (err) {
70 | throw err;
71 | }
72 | }
73 |
74 | private recurseCandidates(
75 | rootPath: string,
76 | format: string,
77 | candidate: string
78 | ): string {
79 | const filename = format.replace('{0}', `.${candidate}`);
80 | const filepath = resolve(rootPath, filename);
81 | if (existsSync(filepath)) {
82 | return filename;
83 | }
84 | if (candidate.split('-')[0] !== candidate) {
85 | return this.recurseCandidates(rootPath, format, candidate.split('-')[0]);
86 | }
87 | return format.replace('{0}', '');
88 | }
89 | }
90 |
91 | export default Localize.prototype.localize.bind(new Localize());
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gwo-android-helper",
3 | "version": "0.0.2",
4 | "displayName": "%gwo.android.extension.name%",
5 | "description": "%gwo.android.extension.description%",
6 | "publisher": "Siubeng",
7 | "galleryBanner": {
8 | "color": "#c6f68d",
9 | "theme": "light"
10 | },
11 | "license": "MIT",
12 | "homepage": "https://github.com/fython/vscode-gwo-android-helper#readme",
13 | "bugs": "https://github.com/fython/vscode-gwo-android-helper/issues",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/fython/vscode-gwo-android-helper.git"
17 | },
18 | "engines": {
19 | "vscode": "^1.53.0"
20 | },
21 | "categories": [
22 | "Other"
23 | ],
24 | "keywords": [
25 | "android",
26 | "adb",
27 | "helper"
28 | ],
29 | "author": {
30 | "name": "Siubeng",
31 | "email": "fythonx@gmail.com"
32 | },
33 | "activationEvents": [
34 | "*"
35 | ],
36 | "main": "./dist/extension.js",
37 | "contributes": {
38 | "commands": [
39 | {
40 | "command": "adbDevices.refresh",
41 | "title": "%gwo.android.devices.refresh%",
42 | "icon": "$(extensions-refresh)"
43 | },
44 | {
45 | "command": "adbDevices.openDeviceInternalStorage",
46 | "title": "%gwo.android.devices.openInternalStorage%",
47 | "icon": "$(extensions-remote)"
48 | },
49 | {
50 | "command": "adbDevices.openDeviceFolder",
51 | "title": "%gwo.android.devices.openFolder%"
52 | },
53 | {
54 | "command": "adbDevices.openTerminal",
55 | "title": "%gwo.android.devices.openTerminal%"
56 | }
57 | ],
58 | "menus": {
59 | "view/title": [
60 | {
61 | "command": "adbDevices.refresh",
62 | "when": "view == adbDevices",
63 | "group": "navigation"
64 | }
65 | ],
66 | "view/item/context": [
67 | {
68 | "command": "adbDevices.openDeviceInternalStorage",
69 | "when": "view == adbDevices && viewItem == adbDevice",
70 | "group": "inline"
71 | },
72 | {
73 | "command": "adbDevices.openDeviceFolder",
74 | "when": "view == adbDevices && viewItem == adbDevice"
75 | },
76 | {
77 | "command": "adbDevices.openTerminal",
78 | "when": "view == adbDevices && viewItem == adbDevice"
79 | }
80 | ]
81 | },
82 | "viewsContainers": {
83 | "activitybar": [
84 | {
85 | "id": "gwo-android-helper",
86 | "title": "%gwo.android.extension.name%",
87 | "icon": "assets/adb.svg"
88 | }
89 | ]
90 | },
91 | "views": {
92 | "gwo-android-helper": [
93 | {
94 | "id": "adbDevices",
95 | "name": "%gwo.android.devices.viewTitle%",
96 | "contextualTitle": "%gwo.android.extension.name%"
97 | }
98 | ]
99 | },
100 | "viewsWelcome": [
101 | {
102 | "view": "adbDevices",
103 | "contents": "%gwo.android.devices.welcome%"
104 | }
105 | ]
106 | },
107 | "scripts": {
108 | "vscode:prepublish": "webpack --mode production --config ./build/webpack.config.js",
109 | "webpack": "webpack --mode development --config ./build/webpack.config.js",
110 | "webpack-dev": "webpack --mode development --watch --config ./build/webpack.config.js",
111 | "compile": "tsc -p ./",
112 | "watch": "tsc -watch -p ./",
113 | "pretest": "npm run compile && npm run lint",
114 | "lint": "eslint src --ext ts"
115 | },
116 | "devDependencies": {
117 | "@types/glob": "^7.1.3",
118 | "@types/mocha": "^8.0.4",
119 | "@types/node": "^12.11.7",
120 | "@types/vscode": "^1.53.0",
121 | "@typescript-eslint/eslint-plugin": "^4.9.0",
122 | "@typescript-eslint/parser": "^4.9.0",
123 | "eslint": "^7.15.0",
124 | "glob": "^7.1.6",
125 | "mocha": "^8.1.3",
126 | "ts-loader": "^8.0.17",
127 | "typescript": "^4.1.2",
128 | "vscode-test": "^1.4.1",
129 | "webpack": "^5.23.0",
130 | "webpack-cli": "^4.5.0"
131 | },
132 | "dependencies": {
133 | "@devicefarmer/adbkit": "^2.11.3",
134 | "concat-stream": "^2.0.0",
135 | "vscode-nls": "^5.0.0"
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/adb.ts:
--------------------------------------------------------------------------------
1 | import { Stats } from 'fs';
2 | import { Readable } from 'stream';
3 | import * as StreamUtils from './utils/stream';
4 | const Adb = require('@devicefarmer/adbkit');
5 | const concatStream = require('concat-stream');
6 |
7 | const client = Adb.createClient();
8 |
9 | function timeout(ms: number): Promise {
10 | return new Promise(async (resolve) => setTimeout(resolve, ms));
11 | }
12 |
13 | export interface AdbDevice {
14 | id: string;
15 | type: AdbDeviceType;
16 | name?: string;
17 | }
18 |
19 | export interface AdbEntry {
20 | name: string;
21 | mode: number;
22 | size: number;
23 | mtime: Date;
24 |
25 | isFile(): boolean;
26 | }
27 |
28 | export type AdbDeviceType = 'device' | 'unauthorized' | 'offline' | 'fastboot' | 'recovery';
29 |
30 | export async function listDevices(): Promise {
31 | const devices: AdbDevice[] = await client.listDevices();
32 | for (const d of devices) {
33 | if (d.type !== 'unauthorized' && d.type !== 'offline') {
34 | d.name = await getProp(d.id, 'ro.product.device');
35 | }
36 | }
37 | return devices;
38 | }
39 |
40 | export function getProp(device: string, key: string): Promise {
41 | return client.shell(device, `getprop ${key}`)
42 | .then(Adb.util.readAll)
43 | .then((out: Buffer) => out.toString().trim());
44 | }
45 |
46 | export function stat(device: string, path: string): Promise {
47 | return client.stat(device, path);
48 | }
49 |
50 | export function readDirectory(device: string, path: string): Promise {
51 | return client.readdir(device, path);
52 | }
53 |
54 | export function readFile(device: string, path: string): Promise {
55 | return new Promise(async (resolve, reject) => {
56 | try {
57 | const transfer = await client.pull(device, path);
58 | const writable = concatStream({ encoding: 'uint8array' }, resolve);
59 | transfer.on('error', reject);
60 | transfer.pipe(writable);
61 | } catch (err) {
62 | reject(err);
63 | }
64 | });
65 | }
66 |
67 | export async function writeFile(device: string, path: string, stream: Readable, overwrite: boolean = false): Promise {
68 | let pathExists = false;
69 | try {
70 | await client.stat(device, path);
71 | pathExists = true;
72 | } catch (ignored) {
73 | // Ignored
74 | }
75 | if (!overwrite && pathExists) {
76 | throw new Error('file exists');
77 | }
78 | return await client.push(device, stream, path);
79 | }
80 |
81 | export function createDirectory(device: string, path: string): Promise {
82 | return client.shell(device, `mkdir "${path}"`)
83 | .then(() => timeout(200));
84 | }
85 |
86 | export async function deleteDirectory(device: string, path: string): Promise {
87 | const out = await client.shell(device, `rmdir "${path}"`).then(Adb.util.read);
88 | const msg = out.toString().trim();
89 | console.log(`shell output: ${msg}`);
90 | if (msg.endsWith(': Directory not empty')) {
91 | throw new Error('non-empty directory can\'t be deleted.');
92 | }
93 | await timeout(200);
94 | return;
95 | }
96 |
97 | export async function deleteFile(device: string, path: string): Promise {
98 | await client.shell(device, `rm "${path}"`);
99 | await timeout(200);
100 | return;
101 | }
102 |
103 | export async function renameInSameDevice(device: string, oldPath: string, newPath: string, options?: { overwrite?: boolean }): Promise {
104 | let newPathExists = false;
105 | try {
106 | await client.stat(device, newPath);
107 | newPathExists = true;
108 | } catch (ignored) {
109 | // Ignored
110 | }
111 | if (options?.overwrite !== true && newPathExists) {
112 | throw new Error('target path already exists');
113 | }
114 | await client.shell(device, `mv "${oldPath}" "${newPath}"`);
115 | await timeout(200);
116 | return;
117 | }
118 |
119 | export async function renameAcrossDevices(oldDevice: string, newDevice: string, oldPath: string, newPath: string, options?: { overwrite?: boolean }): Promise {
120 | let newPathExists = false;
121 | let isFile = false;
122 | try {
123 | const stat = await client.stat(newDevice, newPath);
124 | isFile = stat.isFile();
125 | newPathExists = true;
126 | } catch (ignored) {
127 | // Ignored
128 | }
129 | if (options?.overwrite !== true && newPathExists) {
130 | throw new Error('target path already exists');
131 | }
132 | if (!isFile) {
133 | throw new Error('only files are supported to move across devices');
134 | }
135 | const oldData = await readFile(oldDevice, oldPath);
136 | await writeFile(newDevice, newPath, StreamUtils.toReadable(oldData), options?.overwrite === true);
137 | await deleteFile(oldDevice, oldPath);
138 | }
139 |
--------------------------------------------------------------------------------
/src/fsprovider.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { Stats } from 'fs';
3 | import * as adb from './adb';
4 | import * as StreamUtils from './utils/stream';
5 |
6 | export const ADB_URI_SCHEME = 'adbfile';
7 |
8 | export class AdbUri {
9 | public deviceId: string;
10 | public rootType: string;
11 | public path: string;
12 |
13 | constructor(deviceId: string, rootType: string, path: string) {
14 | this.deviceId = deviceId;
15 | this.rootType = rootType;
16 | if (path.startsWith('/')) {
17 | this.path = path.substr(1);
18 | } else {
19 | this.path = path;
20 | }
21 | }
22 |
23 | get fullPath(): string {
24 | if (this.rootType === 'internal') {
25 | if (this.path === '') {
26 | return '/sdcard';
27 | } else {
28 | return `/sdcard/${this.path}`;
29 | }
30 | } else {
31 | return `/${this.path}`;
32 | }
33 | }
34 |
35 | static fromVSCodeUri(uri: vscode.Uri): AdbUri {
36 | if (uri.scheme !== ADB_URI_SCHEME) {
37 | throw new Error(`the scheme of adb uri should be '${ADB_URI_SCHEME}'`);
38 | }
39 | const parts = uri.path.split('/');
40 | if (parts[0] === '') {
41 | parts.shift();
42 | }
43 | const [deviceId, rootType, ...path] = parts;
44 | return new AdbUri(deviceId, rootType, path.join('/'));
45 | }
46 | }
47 |
48 | export class AdbFileStat implements vscode.FileStat {
49 | type: vscode.FileType;
50 | ctime: number;
51 | mtime: number;
52 | size: number;
53 | name: string;
54 |
55 | constructor(opts: AdbFileStatOptions) {
56 | this.type = opts.type;
57 | this.ctime = opts.ctime ?? Date.now();
58 | this.mtime = opts.mtime ?? Date.now();
59 | this.size = opts.size ?? 0;
60 | this.name = opts.name;
61 | }
62 |
63 | static fromFsStats(stats: Stats): AdbFileStat {
64 | return new AdbFileStat({
65 | type: stats.isFile() ? vscode.FileType.File : vscode.FileType.Directory,
66 | ctime: stats.ctime?.getTime(),
67 | mtime: stats.mtime?.getTime(),
68 | size: stats.isFile() ? stats.size : 0,
69 | name: '',
70 | });
71 | }
72 | }
73 |
74 | interface AdbFileStatOptions {
75 | type: vscode.FileType;
76 | ctime?: number;
77 | mtime?: number;
78 | size?: number;
79 | name: string;
80 | }
81 |
82 | export class AdbFileSystemProvider implements vscode.FileSystemProvider {
83 | private onDidChangeFileEmitter = new vscode.EventEmitter();
84 | readonly onDidChangeFile: vscode.Event = this.onDidChangeFileEmitter.event;
85 |
86 | public isDebugging: boolean = false;
87 |
88 | watch(uri: vscode.Uri, options: { recursive: boolean; excludes: string[]; }): vscode.Disposable {
89 | // TODO unimplemented
90 | return new vscode.Disposable(() => {});
91 | }
92 |
93 | debugLog(message?: any, ...optionalParams: any[]) {
94 | if (this.isDebugging) {
95 | console.log(message, ...optionalParams);
96 | }
97 | }
98 |
99 | async stat(uri: vscode.Uri): Promise {
100 | const adbUri = AdbUri.fromVSCodeUri(uri);
101 | this.debugLog('stat:', adbUri);
102 | try {
103 | const stats = await adb.stat(adbUri.deviceId, adbUri.fullPath);
104 | return AdbFileStat.fromFsStats(stats);
105 | } catch (err) {
106 | if (err.code === 'ENOENT') {
107 | throw vscode.FileSystemError.FileNotFound(uri);
108 | }
109 | throw err;
110 | }
111 | }
112 |
113 | async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
114 | const adbUri = AdbUri.fromVSCodeUri(uri);
115 | this.debugLog('readDirectory:', adbUri);
116 | const entries = await adb.readDirectory(adbUri.deviceId, adbUri.fullPath);
117 | return entries.map((entry: adb.AdbEntry) =>
118 | [entry.name, entry.isFile() ? vscode.FileType.File : vscode.FileType.Directory]);
119 | }
120 |
121 | async createDirectory(uri: vscode.Uri): Promise {
122 | const adbUri = AdbUri.fromVSCodeUri(uri);
123 | this.debugLog('createDirectory:', adbUri);
124 | return await adb.createDirectory(adbUri.deviceId, adbUri.fullPath);
125 | }
126 |
127 | async readFile(uri: vscode.Uri): Promise {
128 | const adbUri = AdbUri.fromVSCodeUri(uri);
129 | this.debugLog('readFile:', adbUri);
130 | return await adb.readFile(adbUri.deviceId, adbUri.fullPath);
131 | }
132 |
133 | async writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): Promise {
134 | const adbUri = AdbUri.fromVSCodeUri(uri);
135 | this.debugLog('writeFile:', adbUri);
136 | return await adb.writeFile(adbUri.deviceId, adbUri.fullPath, StreamUtils.toReadable(content), options.overwrite);
137 | }
138 |
139 | async delete(uri: vscode.Uri, options: { recursive: boolean; }): Promise {
140 | const adbUri = AdbUri.fromVSCodeUri(uri);
141 | this.debugLog('delete:', adbUri);
142 | const stats = await this.stat(uri);
143 | if (stats.type === vscode.FileType.Directory) {
144 | return await adb.deleteDirectory(adbUri.deviceId, adbUri.fullPath);
145 | }
146 | return await adb.deleteFile(adbUri.deviceId, adbUri.fullPath);
147 | }
148 |
149 | async rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; }): Promise {
150 | const oldAdbUri = AdbUri.fromVSCodeUri(oldUri);
151 | const newAdbUri = AdbUri.fromVSCodeUri(newUri);
152 | this.debugLog('rename: %s => %s', oldAdbUri, newAdbUri);
153 | if (oldAdbUri.deviceId !== newAdbUri.deviceId) {
154 | return await adb.renameAcrossDevices(oldAdbUri.deviceId, newAdbUri.deviceId,
155 | oldAdbUri.fullPath, newAdbUri.fullPath, options);
156 | }
157 | return await adb.renameInSameDevice(oldAdbUri.deviceId, oldAdbUri.fullPath, newAdbUri.fullPath, options);
158 | }
159 | }
160 |
--------------------------------------------------------------------------------