Skip to content

Commit a6e871d

Browse files
committed
feat: ADB auto-discovery and one-click install
No more "adb: command not found" — the extension now auto-detects ADB from ANDROID_HOME, common SDK locations, and PATH. If ADB is missing entirely, a dedicated screen lets users install it with one click (no Android Studio required). Gracefully handles ADB disappearing mid-session by showing the install screen instead of cryptic errors.
1 parent a21d730 commit a6e871d

8 files changed

Lines changed: 353 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 2.1.0
2+
3+
### ADB Auto-Discovery & One-Click Install
4+
- **Android Studio is not required** — install ADB directly from within VS Code with a single click
5+
- Automatically finds `adb` from ANDROID_HOME, ANDROID_SDK_ROOT, and common SDK install locations (macOS, Windows, Linux)
6+
- No more "adb: command not found" when VS Code is launched from the dock/start menu instead of a terminal
7+
- New `logcatLens.adbPath` setting to manually override the adb binary path
8+
- Clear "ADB Not Found" screen with Install, Download, and Set Path options when ADB is missing
9+
- Graceful error handling — if ADB is removed or becomes unavailable mid-session, the install screen is shown instead of cryptic errors
10+
111
## 2.0.0
212

313
### App Lifecycle Tracking

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@ A powerful Android Logcat viewer for VS Code — stream, filter, and search devi
2222

2323
## Requirements
2424

25-
- **ADB** installed and in your system PATH
25+
- **ADB** (Android Debug Bridge) — the extension will auto-detect it from common locations, or you can install it directly from within VS Code. **Android Studio is not required.**
2626
- A connected Android device or emulator
2727

28+
### ADB Not Found?
29+
30+
If ADB is not installed, Logcat Lens will prompt you to install it with a single click — no Android Studio needed.
31+
32+
![ADB Install](res/screenshots/adb-install.png)
33+
34+
You can also set a custom ADB path in **Settings > Logcat Lens > Adb Path**.
35+
2836
## Usage
2937

3038
1. Open the **Logcat Lens** tab in the bottom panel

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Logcat Lens",
44
"icon": "res/icon.png",
55
"description": "A powerful Android Logcat viewer for VS Code — stream, filter, and search logs without leaving your editor.",
6-
"version": "2.0.0",
6+
"version": "2.1.0",
77
"publisher": "AshishKumarD",
88
"author": "Ashish Kumar",
99
"engines": {
@@ -57,6 +57,11 @@
5757
"configuration": {
5858
"title": "Logcat Lens",
5959
"properties": {
60+
"logcatLens.adbPath": {
61+
"type": "string",
62+
"default": "",
63+
"description": "Absolute path to the adb binary. Leave empty to auto-detect from ANDROID_HOME or common install locations."
64+
},
6065
"logcatLens.tagGroups": {
6166
"type": "object",
6267
"default": {},

res/screenshots/adb-install.png

70.8 KB
Loading

src/backend/core/adb-service.js

Lines changed: 199 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,192 @@
1-
const { spawn, exec } = require('child_process');
2-
const { platform } = require('os');
1+
const { spawn, exec, execSync } = require('child_process');
2+
const { platform, homedir } = require('os');
3+
const { existsSync, mkdirSync, createWriteStream, chmodSync } = require('fs');
4+
const { join } = require('path');
5+
const https = require('https');
36
const EventEmitter = require('events');
7+
const vscode = require('vscode');
8+
9+
function findAdb() {
10+
// 1. User-configured path takes priority
11+
const configured = vscode.workspace.getConfiguration('logcatLens').get('adbPath');
12+
if (configured && existsSync(configured)) return configured;
13+
14+
// 2. ANDROID_HOME / ANDROID_SDK_ROOT env vars
15+
const home = homedir();
16+
const envDirs = [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT].filter(Boolean);
17+
for (const dir of envDirs) {
18+
const p = join(dir, 'platform-tools', platform() === 'win32' ? 'adb.exe' : 'adb');
19+
if (existsSync(p)) return p;
20+
}
21+
22+
// 3. Common SDK install locations per platform
23+
const candidates = platform() === 'win32' ? [
24+
join(home, 'AppData', 'Local', 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
25+
'C:\\Android\\sdk\\platform-tools\\adb.exe',
26+
] : platform() === 'darwin' ? [
27+
join(home, 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
28+
'/opt/homebrew/bin/adb',
29+
'/usr/local/bin/adb',
30+
] : [
31+
join(home, 'Android', 'Sdk', 'platform-tools', 'adb'),
32+
'/usr/local/bin/adb',
33+
'/usr/bin/adb',
34+
];
35+
36+
for (const p of candidates) {
37+
if (existsSync(p)) return p;
38+
}
39+
40+
// 4. Check if adb is on PATH (works when launched from terminal)
41+
try {
42+
const cmd = platform() === 'win32' ? 'where adb' : 'which adb';
43+
const result = execSync(cmd, { timeout: 5000 }).toString().trim().split('\n')[0];
44+
if (result && existsSync(result)) return result;
45+
} catch { /* not on PATH */ }
46+
47+
// 5. Not found
48+
return null;
49+
}
50+
51+
const PLATFORM_TOOLS_URLS = {
52+
darwin: 'https://dl.google.com/android/repository/platform-tools-latest-darwin.zip',
53+
linux: 'https://dl.google.com/android/repository/platform-tools-latest-linux.zip',
54+
win32: 'https://dl.google.com/android/repository/platform-tools-latest-windows.zip',
55+
};
56+
57+
function isAdbAvailable() {
58+
return findAdb() !== null;
59+
}
60+
61+
async function downloadAndInstallAdb() {
62+
const url = PLATFORM_TOOLS_URLS[platform()];
63+
if (!url) {
64+
vscode.window.showErrorMessage('Unsupported platform for automatic ADB install.');
65+
return false;
66+
}
67+
68+
const home = homedir();
69+
const installDir = platform() === 'win32'
70+
? join(home, 'AppData', 'Local', 'Android', 'Sdk')
71+
: platform() === 'darwin'
72+
? join(home, 'Library', 'Android', 'sdk')
73+
: join(home, 'Android', 'Sdk');
74+
const zipPath = join(installDir, 'platform-tools.zip');
75+
const adbBin = join(installDir, 'platform-tools', platform() === 'win32' ? 'adb.exe' : 'adb');
76+
77+
mkdirSync(installDir, { recursive: true });
78+
79+
return vscode.window.withProgress({
80+
location: vscode.ProgressLocation.Notification,
81+
title: 'Logcat Lens: Installing ADB',
82+
cancellable: false,
83+
}, async (progress) => {
84+
progress.report({ message: 'Downloading platform-tools...' });
85+
86+
await new Promise((resolve, reject) => {
87+
const follow = (url) => {
88+
https.get(url, (res) => {
89+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
90+
return follow(res.headers.location);
91+
}
92+
if (res.statusCode !== 200) {
93+
return reject(new Error(`Download failed (HTTP ${res.statusCode})`));
94+
}
95+
const total = parseInt(res.headers['content-length'], 10) || 0;
96+
let downloaded = 0;
97+
const file = createWriteStream(zipPath);
98+
res.on('data', (chunk) => {
99+
downloaded += chunk.length;
100+
if (total) progress.report({ message: `Downloading... ${Math.round(downloaded / total * 100)}%` });
101+
});
102+
res.pipe(file);
103+
file.on('finish', () => file.close(resolve));
104+
file.on('error', reject);
105+
}).on('error', reject);
106+
};
107+
follow(url);
108+
});
109+
110+
progress.report({ message: 'Extracting...' });
111+
const unzipCmd = platform() === 'win32'
112+
? `powershell -command "Expand-Archive -Force '${zipPath}' '${installDir}'"`
113+
: platform() === 'darwin'
114+
? `ditto -xk "${zipPath}" "${installDir}"`
115+
: `unzip -o "${zipPath}" -d "${installDir}"`;
116+
try {
117+
await new Promise((resolve, reject) => {
118+
exec(unzipCmd, (err) => err ? reject(err) : resolve());
119+
});
120+
} catch (extractErr) {
121+
// Linux fallback: try python3 if unzip isn't installed
122+
if (platform() === 'linux') {
123+
await new Promise((resolve, reject) => {
124+
exec(`python3 -c "import zipfile; zipfile.ZipFile('${zipPath}').extractall('${installDir}')"`,
125+
(err) => err ? reject(err) : resolve());
126+
});
127+
} else {
128+
throw extractErr;
129+
}
130+
}
131+
132+
if (platform() !== 'win32') chmodSync(adbBin, 0o755);
133+
134+
// Clean up zip
135+
try { require('fs').unlinkSync(zipPath); } catch { /* ignore */ }
136+
137+
// Auto-configure the setting and refresh cached path
138+
await vscode.workspace.getConfiguration('logcatLens').update('adbPath', adbBin, vscode.ConfigurationTarget.Global);
139+
_adbPath = adbBin;
140+
_adbWarningShown = false;
141+
142+
vscode.window.showInformationMessage(`ADB installed to ${adbBin}`);
143+
return true;
144+
});
145+
}
146+
147+
let _adbPath;
148+
let _adbWarningShown = false;
149+
function getAdb() {
150+
if (!_adbPath) _adbPath = findAdb();
151+
if (!_adbPath && !_adbWarningShown) {
152+
_adbWarningShown = true;
153+
vscode.window.showErrorMessage(
154+
'ADB not found. Install it directly, download manually, or set the path.',
155+
'Install ADB', 'Download Page', 'Set Path'
156+
).then(choice => {
157+
if (choice === 'Install ADB') {
158+
downloadAndInstallAdb();
159+
} else if (choice === 'Download Page') {
160+
vscode.env.openExternal(vscode.Uri.parse('https://developer.android.com/tools/releases/platform-tools'));
161+
} else if (choice === 'Set Path') {
162+
vscode.commands.executeCommand('workbench.action.openSettings', 'logcatLens.adbPath');
163+
}
164+
});
165+
}
166+
return _adbPath || 'adb';
167+
}
168+
169+
// Reset cached path when settings change
170+
vscode.workspace.onDidChangeConfiguration(e => {
171+
if (e.affectsConfiguration('logcatLens.adbPath')) _adbPath = null;
172+
});
4173

5174
class ADBService extends EventEmitter {
6175
logcatProcess;
7176

177+
_exec(cmd, opts, cb) {
178+
if (typeof opts === 'function') { cb = opts; opts = {}; }
179+
const adb = getAdb();
180+
return exec(cmd.replace(/\badb\b/, `"${adb}"`), opts, cb);
181+
}
182+
183+
_spawn(args) {
184+
return spawn(getAdb(), args);
185+
}
186+
8187
listDevices() {
9188
return new Promise((resolve, reject) => {
10-
exec('adb devices -l', (error, stdout, stderr) => {
189+
this._exec('adb devices -l', (error, stdout, stderr) => {
11190
if (error) return reject(error);
12191

13192
const lines = stdout.split('List of devices attached').pop().trim()
@@ -31,7 +210,7 @@ class ADBService extends EventEmitter {
31210

32211
startDeviceTracking() {
33212
if (this._trackProcess) return;
34-
this._trackProcess = spawn('adb', ['track-devices']);
213+
this._trackProcess = this._spawn(['track-devices']);
35214
this._trackProcess.stdout.on('data', () => {
36215
this.emit('adbevent', { type: 'adb.devices-changed' });
37216
});
@@ -52,7 +231,7 @@ class ADBService extends EventEmitter {
52231

53232
listPackages(deviceId) {
54233
return new Promise((resolve, reject) => {
55-
exec(`adb -s ${deviceId} shell pm list packages -3`, (error, stdout, stderr) => {
234+
this._exec(`adb -s ${deviceId} shell pm list packages -3`, (error, stdout, stderr) => {
56235
if (error) return reject(error);
57236
const packages = stdout.split('\n')
58237
.map(l => l.replace('package:', '').trim())
@@ -65,7 +244,7 @@ class ADBService extends EventEmitter {
65244

66245
listTags(deviceId) {
67246
return new Promise((resolve, reject) => {
68-
exec(`adb -s ${deviceId} logcat -d -v tag`, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
247+
this._exec(`adb -s ${deviceId} logcat -d -v tag`, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
69248
if (error) return reject(error);
70249
const tags = new Set();
71250
stdout.split('\n').forEach(line => {
@@ -80,7 +259,7 @@ class ADBService extends EventEmitter {
80259
getUID(deviceId, packageName) {
81260
return new Promise((resolve, reject) => {
82261
const filter = platform() == 'win32' ? 'FINDSTR' : 'grep';
83-
exec(`adb -s ${deviceId} shell dumpsys package ${packageName} | ${filter} uid`, (error, stdout, stderr) => {
262+
this._exec(`adb -s ${deviceId} shell dumpsys package ${packageName} | ${filter} uid`, (error, stdout, stderr) => {
84263
if (error) return reject(error);
85264

86265
// sample: uid=10520 gids=[] type=0 prot=signature
@@ -93,13 +272,13 @@ class ADBService extends EventEmitter {
93272
getAppState(deviceId, packageName) {
94273
return new Promise((resolve) => {
95274
// Check if process is running
96-
exec(`adb -s ${deviceId} shell pidof ${packageName}`, (err1, pidOut) => {
275+
this._exec(`adb -s ${deviceId} shell pidof ${packageName}`, (err1, pidOut) => {
97276
const pid = pidOut?.trim();
98277
if (!pid) {
99278
return resolve({ packageName, state: 'not-running', pid: null });
100279
}
101280
// Check if it's the foreground app
102-
exec(`adb -s ${deviceId} shell "dumpsys activity activities | grep mResumedActivity"`, (err2, actOut) => {
281+
this._exec(`adb -s ${deviceId} shell "dumpsys activity activities | grep mResumedActivity"`, (err2, actOut) => {
103282
const isForeground = actOut?.includes(packageName);
104283
resolve({
105284
packageName,
@@ -113,7 +292,7 @@ class ADBService extends EventEmitter {
113292

114293
getPackageInfo(deviceId, packageName) {
115294
return new Promise((resolve, reject) => {
116-
exec(`adb -s ${deviceId} shell dumpsys package ${packageName} | grep -E "versionName|versionCode"`, (error, stdout) => {
295+
this._exec(`adb -s ${deviceId} shell dumpsys package ${packageName} | grep -E "versionName|versionCode"`, (error, stdout) => {
117296
if (error) return reject(error);
118297
const version = stdout.match(/versionName=(\S+)/)?.[1] || 'unknown';
119298
const versionCode = stdout.match(/versionCode=(\d+)/)?.[1] || '';
@@ -124,7 +303,7 @@ class ADBService extends EventEmitter {
124303

125304
launchApp(deviceId, packageName) {
126305
return new Promise((resolve, reject) => {
127-
exec(`adb -s ${deviceId} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`, (error) => {
306+
this._exec(`adb -s ${deviceId} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`, (error) => {
128307
if (error) return reject(error);
129308
resolve();
130309
});
@@ -133,7 +312,7 @@ class ADBService extends EventEmitter {
133312

134313
forceStopApp(deviceId, packageName) {
135314
return new Promise((resolve, reject) => {
136-
exec(`adb -s ${deviceId} shell am force-stop ${packageName}`, (error) => {
315+
this._exec(`adb -s ${deviceId} shell am force-stop ${packageName}`, (error) => {
137316
if (error) return reject(error);
138317
resolve();
139318
});
@@ -142,15 +321,15 @@ class ADBService extends EventEmitter {
142321

143322
clearAppData(deviceId, packageName) {
144323
return new Promise((resolve, reject) => {
145-
exec(`adb -s ${deviceId} shell pm clear ${packageName}`, (error) => {
324+
this._exec(`adb -s ${deviceId} shell pm clear ${packageName}`, (error) => {
146325
if (error) return reject(error);
147326
resolve();
148327
});
149328
});
150329
}
151330

152331
refreshPidMap(deviceId) {
153-
exec(`adb -s ${deviceId} shell ps -A -o PID,NAME`, (error, stdout) => {
332+
this._exec(`adb -s ${deviceId} shell ps -A -o PID,NAME`, (error, stdout) => {
154333
if (error) return;
155334
this.pidMap = {};
156335
stdout.split('\n').forEach(line => {
@@ -206,7 +385,7 @@ class ADBService extends EventEmitter {
206385
// -T 1 = only new logs from now, avoids replaying old buffer on stop/start
207386
const args = ['-s', deviceId, 'logcat', '-T', '1', '*:V'];
208387

209-
this.logcatProcess = spawn('adb', args);
388+
this.logcatProcess = this._spawn(args);
210389

211390
// Buffer partial lines across data chunks
212391
let lineBuffer = '';
@@ -354,9 +533,12 @@ class ADBService extends EventEmitter {
354533

355534
clear(deviceId) {
356535
const device = deviceId || this.lastParams?.deviceId;
357-
if (device) exec(`adb -s ${device} logcat -c`);
358-
else exec(`adb logcat -c`);
536+
if (device) this._exec(`adb -s ${device} logcat -c`);
537+
else this._exec(`adb logcat -c`);
359538
}
360539
}
361540

362541
module.exports = ADBService;
542+
module.exports.isAdbAvailable = isAdbAvailable;
543+
module.exports.downloadAndInstallAdb = downloadAndInstallAdb;
544+
module.exports.resetAdbCache = () => { _adbPath = null; _adbWarningShown = false; };

0 commit comments

Comments
 (0)