Skip to content

Commit a21d730

Browse files
committed
feat: app lifecycle tracking, tag groups, search filter mode, and bug fixes
1 parent f53dfc5 commit a21d730

8 files changed

Lines changed: 895 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1+
## 2.0.0
2+
3+
### App Lifecycle Tracking
4+
- Real-time app state monitoring (Foreground, Background, Not Running, Started, Killed, Crashed, ANR)
5+
- Lifecycle events appear as colored banner rows in the log stream with "Logcat Lens" tag
6+
- Custom `L` log level for Logcat Lens events — always visible through tag/package/search filters, toggleable via level chip
7+
- Context-aware action buttons in status bar: Launch, Force Stop, Bring to Front, Clear Data
8+
9+
### Tag Groups
10+
- Save current tags as a named group, load or delete saved groups
11+
- Active group shown as a collapsible chip — click to expand/collapse individual tags
12+
- Groups persist in VS Code settings across sessions
13+
14+
### Search & Filtering
15+
- Search filter mode — toggle to show only matching logs instead of just navigating
16+
- Search now respects active tag/package/level filters
17+
- Export respects active filters instead of dumping the entire buffer
18+
- Level chip tooltips showing full level names
19+
20+
### Device Detection
21+
- Auto-detect device connect/disconnect via `adb track-devices`
22+
- Online/offline/unauthorized status shown in device dropdown
23+
- Refresh now reliably picks up new devices
24+
25+
### Package Tracking
26+
- Auto-detect package install/uninstall/update from logcat
27+
- Immediate PID map refresh on package changes
28+
- App version info fetched on package selection
29+
30+
### Bug Fixes
31+
- Copy now works in virtual scroll mode (normal mode)
32+
- Search navigation works correctly in soft-wrap mode
33+
- No more duplicate logs when stopping and restarting streaming
34+
- Tag/package chip containers scroll when many are selected
35+
- Device-specific `adb logcat -c` instead of default device
36+
- Tooltips near screen edge flip to stay visible
37+
138
## 1.0.0
239

340
- Real-time logcat streaming with pause/resume/restart

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ A powerful Android Logcat viewer for VS Code — stream, filter, and search devi
88

99
- **Real-time streaming** — Stream logs from any connected device or emulator
1010
- **Instant filtering** — Toggle log levels, add tag/package filters with autocomplete
11-
- **Search** — Full-text search with match counter and navigation
11+
- **Search** — Full-text search with match counter, navigation, and filter mode
1212
- **Display modes** — Standard, compact, and soft wrap with infinite scroll-back
1313
- **High performance** — Smooth scrolling even with massive log volumes
14-
- **Export & copy** — Double-click to copy, export to editor
14+
- **Export & copy** — Double-click to copy, export filtered logs to editor
15+
- **App lifecycle tracking** — Real-time app state (Foreground, Background, Killed, Crashed, ANR) with action buttons
16+
- **Tag groups** — Save and load named groups of tags for quick switching
17+
- **Device monitoring** — Auto-detect device connect/disconnect with online/offline status
18+
19+
![Lifecycle & Tag Groups](res/screenshots/lifecycle.png)
1520

1621
![Filtering](res/screenshots/filtering.png)
1722

@@ -26,3 +31,5 @@ A powerful Android Logcat viewer for VS Code — stream, filter, and search devi
2631
2. Select a device and click play to start streaming
2732
3. Filter by level, tag, or package using the filter bar
2833
4. Use sidebar buttons to pause, clear, wrap, or export
34+
5. Select a single package to enable lifecycle tracking and app actions
35+
6. Save frequently-used tag sets as groups for quick recall

package.json

Lines changed: 11 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": "1.0.1",
6+
"version": "2.0.0",
77
"publisher": "AshishKumarD",
88
"author": "Ashish Kumar",
99
"engines": {
@@ -53,6 +53,16 @@
5353
"icon": ""
5454
}
5555
]
56+
},
57+
"configuration": {
58+
"title": "Logcat Lens",
59+
"properties": {
60+
"logcatLens.tagGroups": {
61+
"type": "object",
62+
"default": {},
63+
"description": "Saved tag groups. Keys are group names, values are arrays of tag strings."
64+
}
65+
}
5666
}
5767
},
5868
"scripts": {

res/screenshots/lifecycle.png

250 KB
Loading

src/backend/core/adb-service.js

Lines changed: 202 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,46 @@ class ADBService extends EventEmitter {
1010
exec('adb devices -l', (error, stdout, stderr) => {
1111
if (error) return reject(error);
1212

13-
// sample: R5CX912W25A device product:e3qxxx model:SM_S928B device:e3q transport_id:1
1413
const lines = stdout.split('List of devices attached').pop().trim()
1514
.split('\n').map(l => l.trim()).filter(l => l);
1615

1716
const devices = lines.map(line => {
18-
const [id, model] = line.match(/(^\w+)|(model:\w+)/g);
19-
return { id, model: model.split(':').pop(), raw: line };
20-
});
17+
const matches = line.match(/(^\S+)/);
18+
if (!matches) return null;
19+
const id = matches[1];
20+
const modelMatch = line.match(/model:(\S+)/);
21+
const model = modelMatch ? modelMatch[1] : id;
22+
const status = line.includes('unauthorized') ? 'unauthorized'
23+
: line.includes('offline') ? 'offline' : 'online';
24+
return { id, model, status, raw: line };
25+
}).filter(Boolean);
2126

2227
resolve(devices);
2328
});
2429
});
2530
}
2631

32+
startDeviceTracking() {
33+
if (this._trackProcess) return;
34+
this._trackProcess = spawn('adb', ['track-devices']);
35+
this._trackProcess.stdout.on('data', () => {
36+
this.emit('adbevent', { type: 'adb.devices-changed' });
37+
});
38+
this._trackProcess.on('close', () => {
39+
this._trackProcess = null;
40+
this._trackRetry = setTimeout(() => this.startDeviceTracking(), 3000);
41+
});
42+
this._trackProcess.on('error', () => {
43+
this._trackProcess = null;
44+
});
45+
}
46+
47+
stopDeviceTracking() {
48+
clearTimeout(this._trackRetry);
49+
this._trackProcess?.kill();
50+
this._trackProcess = null;
51+
}
52+
2753
listPackages(deviceId) {
2854
return new Promise((resolve, reject) => {
2955
exec(`adb -s ${deviceId} shell pm list packages -3`, (error, stdout, stderr) => {
@@ -64,6 +90,65 @@ class ADBService extends EventEmitter {
6490
});
6591
}
6692

93+
getAppState(deviceId, packageName) {
94+
return new Promise((resolve) => {
95+
// Check if process is running
96+
exec(`adb -s ${deviceId} shell pidof ${packageName}`, (err1, pidOut) => {
97+
const pid = pidOut?.trim();
98+
if (!pid) {
99+
return resolve({ packageName, state: 'not-running', pid: null });
100+
}
101+
// Check if it's the foreground app
102+
exec(`adb -s ${deviceId} shell "dumpsys activity activities | grep mResumedActivity"`, (err2, actOut) => {
103+
const isForeground = actOut?.includes(packageName);
104+
resolve({
105+
packageName,
106+
state: isForeground ? 'foreground' : 'background',
107+
pid,
108+
});
109+
});
110+
});
111+
});
112+
}
113+
114+
getPackageInfo(deviceId, packageName) {
115+
return new Promise((resolve, reject) => {
116+
exec(`adb -s ${deviceId} shell dumpsys package ${packageName} | grep -E "versionName|versionCode"`, (error, stdout) => {
117+
if (error) return reject(error);
118+
const version = stdout.match(/versionName=(\S+)/)?.[1] || 'unknown';
119+
const versionCode = stdout.match(/versionCode=(\d+)/)?.[1] || '';
120+
resolve({ packageName, version, versionCode });
121+
});
122+
});
123+
}
124+
125+
launchApp(deviceId, packageName) {
126+
return new Promise((resolve, reject) => {
127+
exec(`adb -s ${deviceId} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`, (error) => {
128+
if (error) return reject(error);
129+
resolve();
130+
});
131+
});
132+
}
133+
134+
forceStopApp(deviceId, packageName) {
135+
return new Promise((resolve, reject) => {
136+
exec(`adb -s ${deviceId} shell am force-stop ${packageName}`, (error) => {
137+
if (error) return reject(error);
138+
resolve();
139+
});
140+
});
141+
}
142+
143+
clearAppData(deviceId, packageName) {
144+
return new Promise((resolve, reject) => {
145+
exec(`adb -s ${deviceId} shell pm clear ${packageName}`, (error) => {
146+
if (error) return reject(error);
147+
resolve();
148+
});
149+
});
150+
}
151+
67152
refreshPidMap(deviceId) {
68153
exec(`adb -s ${deviceId} shell ps -A -o PID,NAME`, (error, stdout) => {
69154
if (error) return;
@@ -88,8 +173,38 @@ class ADBService extends EventEmitter {
88173
this.refreshPidMap(deviceId);
89174
this._pidInterval = setInterval(() => this.refreshPidMap(deviceId), 30000);
90175

176+
// App lifecycle tracking for single-package mode
177+
if (packages && packages.length === 1) {
178+
const pkg = packages[0];
179+
this._lastAppState = null;
180+
this._appStateRunning = true;
181+
182+
const pollLoop = () => {
183+
if (!this._appStateRunning) return;
184+
this.getAppState(deviceId, pkg).then(state => {
185+
if (state.state !== this._lastAppState) {
186+
this._lastAppState = state.state;
187+
this.emit('adbevent', {
188+
type: 'adb.lifecycle',
189+
data: { event: state.state, pkg, detail: `${state.state} (PID: ${state.pid || 'none'})` }
190+
});
191+
}
192+
// Schedule next check after a short delay (serialized, no overlap)
193+
if (this._appStateRunning) {
194+
this._appStateTimeout = setTimeout(pollLoop, 100);
195+
}
196+
}).catch(() => {
197+
if (this._appStateRunning) {
198+
this._appStateTimeout = setTimeout(pollLoop, 500);
199+
}
200+
});
201+
};
202+
pollLoop();
203+
}
204+
91205
// Always stream everything at Verbose — all filtering is client-side
92-
const args = ['-s', deviceId, 'logcat', '*:V'];
206+
// -T 1 = only new logs from now, avoids replaying old buffer on stop/start
207+
const args = ['-s', deviceId, 'logcat', '-T', '1', '*:V'];
93208

94209
this.logcatProcess = spawn('adb', args);
95210

@@ -127,6 +242,81 @@ class ADBService extends EventEmitter {
127242
type: 'adb.log',
128243
data: batch[i]
129244
});
245+
246+
// Detect package install/uninstall/update events
247+
const tag = batch[i].tag?.trim();
248+
if ((tag === 'PackageManager' || tag === 'PackageInstaller') &&
249+
/\b(install|uninstall|remove|replace|update)\b/i.test(batch[i].message)) {
250+
this.refreshPidMap(deviceId);
251+
setTimeout(() => this.refreshPidMap(deviceId), 2000);
252+
this.emit('adbevent', {
253+
type: 'adb.package-changed',
254+
data: { message: batch[i].message, tag }
255+
});
256+
}
257+
258+
// Detect app lifecycle events for tracked packages
259+
const msg = batch[i].message;
260+
const trackedPkgs = this.lastParams?.packages || [];
261+
if (trackedPkgs.length > 0) {
262+
let lifecycle = null;
263+
if (tag === 'ActivityManager') {
264+
for (const pkg of trackedPkgs) {
265+
if (msg.includes(pkg)) {
266+
if (/^Start proc\b/.test(msg)) {
267+
lifecycle = { event: 'started', pkg, detail: msg };
268+
this.refreshPidMap(deviceId);
269+
setTimeout(() => this.refreshPidMap(deviceId), 1500);
270+
} else if (/^Displayed\b/.test(msg)) {
271+
lifecycle = { event: 'displayed', pkg, detail: msg };
272+
} else if (/\bKilling\b/.test(msg)) {
273+
lifecycle = { event: 'killed', pkg, detail: msg };
274+
this.refreshPidMap(deviceId);
275+
} else if (/\bANR in\b/.test(msg)) {
276+
lifecycle = { event: 'anr', pkg, detail: msg };
277+
} else if (/\bForce stopping\b/.test(msg)) {
278+
lifecycle = { event: 'force-stopped', pkg, detail: msg };
279+
} else if (/\bProcess .* has died\b/.test(msg)) {
280+
lifecycle = { event: 'died', pkg, detail: msg };
281+
this.refreshPidMap(deviceId);
282+
}
283+
break;
284+
}
285+
}
286+
} else if (tag === 'ActivityTaskManager') {
287+
for (const pkg of trackedPkgs) {
288+
if (msg.includes(pkg)) {
289+
if (/\bmovedToFront\b|\btopResumedActivity\b|\bResume\b.*\bActivity\b/.test(msg)) {
290+
lifecycle = { event: 'foreground', pkg, detail: msg };
291+
} else if (/\bmoveToBack\b|\bPause\b.*\bActivity\b/.test(msg)) {
292+
lifecycle = { event: 'background', pkg, detail: msg };
293+
}
294+
break;
295+
}
296+
}
297+
} else if (tag === 'AndroidRuntime' && /^FATAL EXCEPTION/.test(msg)) {
298+
const crashPkg = this.pidMap?.[batch[i].pid];
299+
if (crashPkg && trackedPkgs.some(p => crashPkg.includes(p))) {
300+
lifecycle = { event: 'crashed', pkg: crashPkg, detail: msg };
301+
}
302+
}
303+
// Also detect resume/pause from the app's own Activity logs
304+
if (!lifecycle && trackedPkgs.length === 1) {
305+
const appPkg = this.pidMap?.[batch[i].pid];
306+
if (appPkg && trackedPkgs.some(p => appPkg.includes(p))) {
307+
if (/\bonResume\b/.test(msg)) {
308+
lifecycle = { event: 'resumed', pkg: appPkg, detail: `${tag}: ${msg}` };
309+
} else if (/\bonPause\b/.test(msg)) {
310+
lifecycle = { event: 'paused', pkg: appPkg, detail: `${tag}: ${msg}` };
311+
} else if (/\bonStop\b/.test(msg)) {
312+
lifecycle = { event: 'stopped', pkg: appPkg, detail: `${tag}: ${msg}` };
313+
}
314+
}
315+
}
316+
if (lifecycle) {
317+
this.emit('adbevent', { type: 'adb.lifecycle', data: lifecycle });
318+
}
319+
}
130320
}
131321
});
132322

@@ -151,6 +341,9 @@ class ADBService extends EventEmitter {
151341
this.logcatProcess?.kill();
152342
this.logcatProcess = null;
153343
clearInterval(this._pidInterval);
344+
this._appStateRunning = false;
345+
clearTimeout(this._appStateTimeout);
346+
this._lastAppState = null;
154347
}
155348

156349
async restart(params) {
@@ -159,8 +352,10 @@ class ADBService extends EventEmitter {
159352
await this.start(params || this.lastParams);
160353
}
161354

162-
clear() {
163-
exec(`adb logcat -c`);
355+
clear(deviceId) {
356+
const device = deviceId || this.lastParams?.deviceId;
357+
if (device) exec(`adb -s ${device} logcat -c`);
358+
else exec(`adb logcat -c`);
164359
}
165360
}
166361

0 commit comments

Comments
 (0)