Skip to content

Commit f960ddc

Browse files
committed
chore: Refactoring Adb to keep separated logics
- Created a new class "HeadsetSetup" managing all the device checks/application, etc etc - Keep the `AdbManager` a manager, not doing any ADB check
1 parent d0fa3c8 commit f960ddc

2 files changed

Lines changed: 185 additions & 156 deletions

File tree

src/api/android/adb/AdbManager.ts

Lines changed: 7 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,14 @@
99

1010
import { Adb, AdbServerClient } from "@yume-chan/adb";
1111
import { AdbServerNodeTcpConnector } from "@yume-chan/adb-server-node-tcp";
12-
import { ReadableStream } from "@yume-chan/stream-extra";
13-
import { PackageManager } from "@yume-chan/android-bin";
1412
import Device = AdbServerClient.Device;
1513

16-
import { readFile, stat } from "node:fs/promises";
17-
import { resolve } from "node:path";
18-
1914
import Controller from "../../core/Controller.ts";
2015
import { ENV_EXTRA_VERBOSE, ENV_VERBOSE } from "../../index.ts";
2116
import { ScrcpyServer } from "../scrcpy/ScrcpyServer.ts";
2217
import { getLogger } from "@logtape/logtape";
2318
import DeviceFinder from "./DeviceFinder.ts";
24-
import {ON_DEVICE_ADB_GLOBAL_SETTINGS} from "../../core/Constants.ts";
19+
import { HeadsetSetup } from "./HeadsetSetup.ts";
2520

2621
// Override the log function
2722
const logger = getLogger(["android", "AdbManager"]);
@@ -30,6 +25,7 @@ export class AdbManager {
3025
controller: Controller;
3126
adbServer!: AdbServerClient;
3227
videoStreamServer: ScrcpyServer;
28+
headsetSetup!: HeadsetSetup;
3329
// Keep list of serial of devices with a stream already starting
3430
clientCurrentlyStreaming: Device[] = [];
3531
observer!: AdbServerClient.DeviceObserver;//!: AdbServerClient;
@@ -46,6 +42,7 @@ export class AdbManager {
4642
logger.info("Connect to device's ADB server");
4743

4844
this.videoStreamServer = new ScrcpyServer(this);
45+
this.headsetSetup = new HeadsetSetup(this.adbServer);
4946
}
5047

5148
async init() {
@@ -61,8 +58,8 @@ export class AdbManager {
6158
// Sanitize stale ADB entries on startup (side-effect: disconnects offline ones)
6259
if (this.isDeviceReady(device)) {
6360
// Apply M2L2 headset settings only if the device is ready
64-
this.checkAdbParameters(device).catch(e =>
65-
logger.error(`[${device.serial}] Unexpected error in checkAdbParameters: {e}`, { e })
61+
this.headsetSetup.setupHeadset(device).catch(e =>
62+
logger.error(`[${device.serial}] Unexpected error in setupHeadset: {e}`, { e })
6663
);
6764
}
6865

@@ -81,8 +78,8 @@ export class AdbManager {
8178
this.startNewStream(device).catch(e =>
8279
logger.error(`[${device.serial}] Unexpected error in startNewStream: {e}`, { e })
8380
);
84-
this.checkAdbParameters(device).catch(e =>
85-
logger.error(`[${device.serial}] Unexpected error in checkAdbParameters: {e}`, { e })
81+
this.headsetSetup.setupHeadset(device).catch(e =>
82+
logger.error(`[${device.serial}] Unexpected error in setupHeadset: {e}`, { e })
8683
);
8784
}
8885
});
@@ -256,150 +253,4 @@ export class AdbManager {
256253
return success
257254
}
258255

259-
async checkAdbParameters(device: Device) {
260-
if (!this.isDeviceReady(device)) return;
261-
262-
// Only modify headsets, don't jam phone's parameters
263-
if (!device.model?.startsWith("Quest_")) return;
264-
265-
logger.debug(`[${device.serial}] Checking on-device global ADB settings...`);
266-
267-
let adb: Adb;
268-
try {
269-
adb = new Adb(await this.adbServer.createTransport(device));
270-
} catch (e) {
271-
logger.warn(`[${device.serial}] Could not open ADB transport to check parameters — device may not be authorized yet: {e}`, { e });
272-
return;
273-
}
274-
275-
for (const [globalSetting, globalSettingValue] of Object.entries(ON_DEVICE_ADB_GLOBAL_SETTINGS) as [
276-
keyof typeof ON_DEVICE_ADB_GLOBAL_SETTINGS,
277-
typeof ON_DEVICE_ADB_GLOBAL_SETTINGS[keyof typeof ON_DEVICE_ADB_GLOBAL_SETTINGS]
278-
][])
279-
{
280-
if ( ! await this.checkAdbParameter(adb, globalSetting, globalSettingValue)){
281-
logger.debug(`[${device.serial}] ADB parameters for '${globalSetting}' isn't correct, fixing it...`);
282-
await this.setAdbParameter(adb, globalSetting, globalSettingValue);
283-
if (! await this.checkAdbParameter(adb, globalSetting, globalSettingValue)) {
284-
logger.warn(`[${device.serial}] Couldn't properly set setting ${globalSetting}, skipping it...`);
285-
}
286-
}
287-
}
288-
logger.debug(`[${device.serial}] All on-device global ADB settings are good`);
289-
290-
await this.checkRequiredApps(adb, device.serial);
291-
}
292-
293-
async checkRequiredApps(adb: Adb, serial: string) {
294-
const REQUIRED_APP = "com.tpn.adbautoenable";
295-
const REQUIRED_PERMISSION = "android.permission.WRITE_SECURE_SETTINGS";
296-
297-
logger.debug(`[${serial}] Checking required app '${REQUIRED_APP}'...`);
298-
299-
// Check if app is installed
300-
const pmProcess = await adb.subprocess.noneProtocol.spawn(`pm list packages ${REQUIRED_APP}`);
301-
let pmOutput = "";
302-
// @ts-expect-error
303-
for await (const chunk of pmProcess.output.pipeThrough(new TextDecoderStream())) {
304-
pmOutput += chunk;
305-
}
306-
307-
if (!pmOutput.includes(`package:${REQUIRED_APP}`)) {
308-
logger.warn(`[${serial}] Required app '${REQUIRED_APP}' is not installed — installing it...`);
309-
const installed = await this.installApk(adb, serial, resolve("toolkit", `${REQUIRED_APP}.apk`));
310-
if (!installed) return;
311-
}
312-
logger.debug(`[${serial}] '${REQUIRED_APP}' is installed`);
313-
314-
// Check if WRITE_SECURE_SETTINGS is already granted
315-
const dumpsysProcess = await adb.subprocess.noneProtocol.spawn(`dumpsys package ${REQUIRED_APP}`);
316-
let dumpsysOutput = "";
317-
// @ts-expect-error
318-
for await (const chunk of dumpsysProcess.output.pipeThrough(new TextDecoderStream())) {
319-
dumpsysOutput += chunk;
320-
}
321-
322-
// The permission line looks like: "android.permission.WRITE_SECURE_SETTINGS: granted=true"
323-
const permissionGranted = dumpsysOutput.includes(`${REQUIRED_PERMISSION}: granted=true`);
324-
325-
if (permissionGranted) {
326-
logger.debug(`[${serial}] '${REQUIRED_APP}' already has ${REQUIRED_PERMISSION}`);
327-
return;
328-
}
329-
330-
logger.debug(`[${serial}] Granting ${REQUIRED_PERMISSION} to '${REQUIRED_APP}'...`);
331-
const grantProcess = await adb.subprocess.noneProtocol.spawn(`pm grant ${REQUIRED_APP} ${REQUIRED_PERMISSION}`);
332-
let grantOutput = "";
333-
// @ts-expect-error
334-
for await (const chunk of grantProcess.output.pipeThrough(new TextDecoderStream())) {
335-
grantOutput += chunk;
336-
}
337-
338-
if (grantOutput.trim()) {
339-
logger.error(`[${serial}] Failed to grant ${REQUIRED_PERMISSION} to '${REQUIRED_APP}': ${grantOutput.trim()}`);
340-
} else {
341-
logger.info(`[${serial}] Successfully granted ${REQUIRED_PERMISSION} to '${REQUIRED_APP}'`);
342-
}
343-
}
344-
345-
async installApk(adb: Adb, serial: string, apkPath: string): Promise<boolean> {
346-
let apkBytes: Uint8Array;
347-
let apkSize: number;
348-
try {
349-
apkBytes = new Uint8Array(await readFile(apkPath));
350-
apkSize = (await stat(apkPath)).size;
351-
} catch (e) {
352-
logger.error(`[${serial}] Could not read APK at '${apkPath}': {e}`, { e });
353-
return false;
354-
}
355-
356-
try {
357-
const pm = new PackageManager(adb);
358-
await pm.installStream(apkSize, new ReadableStream({
359-
start: (controller) => {
360-
controller.enqueue(apkBytes);
361-
controller.close();
362-
},
363-
}));
364-
logger.info(`[${serial}] Successfully installed APK '${apkPath}'`);
365-
return true;
366-
} catch (e) {
367-
logger.error(`[${serial}] Failed to install APK '${apkPath}': {e}`, { e });
368-
return false;
369-
}
370-
}
371-
372-
async checkAdbParameter(adb: Adb, globalParam: string, expectedValue: any): Promise<boolean> {
373-
let result: any;
374-
375-
const process = await adb.subprocess.noneProtocol.spawn("settings get global " + globalParam);
376-
// @ts-expect-error
377-
for await (const chunk of process.output.pipeThrough(new TextDecoderStream())) {
378-
result = chunk;
379-
}
380-
// Cleaning trailing '\n' from chunk reading
381-
result = result.trim();
382-
383-
logger.trace(`[${adb.serial}] Checking ADB parameters '${globalParam}' = ${result} and should be ${expectedValue} (${result == expectedValue})`);
384-
385-
return result == expectedValue;
386-
}
387-
388-
async setAdbParameter(adb: Adb, globalParam: string, expectedValue: any) {
389-
let result: any;
390-
391-
const process = await adb.subprocess.noneProtocol.spawn(["settings put global ", globalParam, expectedValue]);
392-
// @ts-expect-error
393-
for await (const chunk of process.output.pipeThrough(new TextDecoderStream())) {
394-
result = chunk;
395-
}
396-
397-
logger.trace(`[${adb.serial}] Setting ADB parameters '${globalParam}' = ${expectedValue} and it ${result == undefined ? "worked" : "failed"}`);
398-
399-
if (result != undefined) {
400-
logger.error(`[${adb.serial}] Something happened while setting the ADB setting '${globalParam}'`);
401-
logger.error(result.toString());
402-
}
403-
}
404-
405256
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
2+
/**
3+
* HeadsetSetup.ts
4+
* ===============
5+
*
6+
* Description:
7+
* Handles provisioning of Quest headsets on connection:
8+
* 1. Apply required global ADB settings
9+
* 2. Ensure required apps are installed and correctly configured
10+
*/
11+
12+
import { Adb, AdbServerClient } from "@yume-chan/adb";
13+
import { ReadableStream } from "@yume-chan/stream-extra";
14+
import { PackageManager } from "@yume-chan/android-bin";
15+
import Device = AdbServerClient.Device;
16+
17+
import { readFile, stat } from "node:fs/promises";
18+
import { resolve } from "node:path";
19+
20+
import { getLogger } from "@logtape/logtape";
21+
import { ON_DEVICE_ADB_GLOBAL_SETTINGS } from "../../core/Constants.ts";
22+
23+
const logger = getLogger(["android", "HeadsetSetup"]);
24+
25+
export class HeadsetSetup {
26+
private adbServer: AdbServerClient;
27+
28+
constructor(adbServer: AdbServerClient) {
29+
this.adbServer = adbServer;
30+
}
31+
32+
async setupHeadset(device: Device) {
33+
// Only provision Quest headsets
34+
if (!device.model?.startsWith("Quest_")) return;
35+
36+
let adb: Adb;
37+
try {
38+
adb = new Adb(await this.adbServer.createTransport(device));
39+
} catch (e) {
40+
logger.warn(`[${device.serial}] Could not open ADB transport — device may not be authorized yet: {e}`, { e });
41+
return;
42+
}
43+
44+
await this.applyGlobalSettings(adb, device.serial);
45+
await this.checkRequiredApps(adb, device.serial);
46+
}
47+
48+
private async applyGlobalSettings(adb: Adb, serial: string) {
49+
logger.debug(`[${serial}] Checking on-device global ADB settings...`);
50+
51+
for (const [globalSetting, globalSettingValue] of Object.entries(ON_DEVICE_ADB_GLOBAL_SETTINGS) as [
52+
keyof typeof ON_DEVICE_ADB_GLOBAL_SETTINGS,
53+
typeof ON_DEVICE_ADB_GLOBAL_SETTINGS[keyof typeof ON_DEVICE_ADB_GLOBAL_SETTINGS]
54+
][])
55+
{
56+
if (!await this.checkGlobalSetting(adb, globalSetting, globalSettingValue)) {
57+
logger.debug(`[${serial}] ADB parameters for '${globalSetting}' isn't correct, fixing it...`);
58+
await this.setGlobalSetting(adb, globalSetting, globalSettingValue);
59+
if (!await this.checkGlobalSetting(adb, globalSetting, globalSettingValue)) {
60+
logger.warn(`[${serial}] Couldn't properly set setting ${globalSetting}, skipping it...`);
61+
}
62+
}
63+
}
64+
65+
logger.debug(`[${serial}] All on-device global ADB settings are good`);
66+
}
67+
68+
private async checkGlobalSetting(adb: Adb, globalParam: string, expectedValue: any): Promise<boolean> {
69+
let result: any;
70+
71+
const process = await adb.subprocess.noneProtocol.spawn("settings get global " + globalParam);
72+
// @ts-expect-error
73+
for await (const chunk of process.output.pipeThrough(new TextDecoderStream())) {
74+
result = chunk;
75+
}
76+
result = result.trim();
77+
78+
logger.trace(`[${adb.serial}] Checking ADB parameters '${globalParam}' = ${result} and should be ${expectedValue} (${result == expectedValue})`);
79+
80+
return result == expectedValue;
81+
}
82+
83+
private async setGlobalSetting(adb: Adb, globalParam: string, expectedValue: any) {
84+
let result: any;
85+
86+
const process = await adb.subprocess.noneProtocol.spawn(["settings put global ", globalParam, expectedValue]);
87+
// @ts-expect-error
88+
for await (const chunk of process.output.pipeThrough(new TextDecoderStream())) {
89+
result = chunk;
90+
}
91+
92+
logger.trace(`[${adb.serial}] Setting ADB parameters '${globalParam}' = ${expectedValue} and it ${result == undefined ? "worked" : "failed"}`);
93+
94+
if (result != undefined) {
95+
logger.error(`[${adb.serial}] Something happened while setting the ADB setting '${globalParam}'`);
96+
logger.error(result.toString());
97+
}
98+
}
99+
100+
private async checkRequiredApps(adb: Adb, serial: string) {
101+
const REQUIRED_APP = "com.tpn.adbautoenable";
102+
const REQUIRED_PERMISSION = "android.permission.WRITE_SECURE_SETTINGS";
103+
104+
logger.debug(`[${serial}] Checking required app '${REQUIRED_APP}'...`);
105+
106+
// Check if app is installed
107+
const pmProcess = await adb.subprocess.noneProtocol.spawn(`pm list packages ${REQUIRED_APP}`);
108+
let pmOutput = "";
109+
// @ts-expect-error
110+
for await (const chunk of pmProcess.output.pipeThrough(new TextDecoderStream())) {
111+
pmOutput += chunk;
112+
}
113+
114+
if (!pmOutput.includes(`package:${REQUIRED_APP}`)) {
115+
logger.warn(`[${serial}] Required app '${REQUIRED_APP}' is not installed — installing it...`);
116+
const installed = await this.installApk(adb, serial, resolve("toolkit", `${REQUIRED_APP}.apk`));
117+
if (!installed) return;
118+
}
119+
logger.debug(`[${serial}] '${REQUIRED_APP}' is installed`);
120+
121+
// Check if WRITE_SECURE_SETTINGS is already granted
122+
const dumpsysProcess = await adb.subprocess.noneProtocol.spawn(`dumpsys package ${REQUIRED_APP}`);
123+
let dumpsysOutput = "";
124+
// @ts-expect-error
125+
for await (const chunk of dumpsysProcess.output.pipeThrough(new TextDecoderStream())) {
126+
dumpsysOutput += chunk;
127+
}
128+
129+
// The permission line looks like: "android.permission.WRITE_SECURE_SETTINGS: granted=true"
130+
const permissionGranted = dumpsysOutput.includes(`${REQUIRED_PERMISSION}: granted=true`);
131+
132+
if (permissionGranted) {
133+
logger.debug(`[${serial}] '${REQUIRED_APP}' already has ${REQUIRED_PERMISSION}`);
134+
return;
135+
}
136+
137+
logger.debug(`[${serial}] Granting ${REQUIRED_PERMISSION} to '${REQUIRED_APP}'...`);
138+
const grantProcess = await adb.subprocess.noneProtocol.spawn(`pm grant ${REQUIRED_APP} ${REQUIRED_PERMISSION}`);
139+
let grantOutput = "";
140+
// @ts-expect-error
141+
for await (const chunk of grantProcess.output.pipeThrough(new TextDecoderStream())) {
142+
grantOutput += chunk;
143+
}
144+
145+
if (grantOutput.trim()) {
146+
logger.error(`[${serial}] Failed to grant ${REQUIRED_PERMISSION} to '${REQUIRED_APP}': ${grantOutput.trim()}`);
147+
} else {
148+
logger.info(`[${serial}] Successfully granted ${REQUIRED_PERMISSION} to '${REQUIRED_APP}'`);
149+
}
150+
}
151+
152+
private async installApk(adb: Adb, serial: string, apkPath: string): Promise<boolean> {
153+
let apkBytes: Uint8Array;
154+
let apkSize: number;
155+
try {
156+
apkBytes = new Uint8Array(await readFile(apkPath));
157+
apkSize = (await stat(apkPath)).size;
158+
} catch (e) {
159+
logger.error(`[${serial}] Could not read APK at '${apkPath}': {e}`, { e });
160+
return false;
161+
}
162+
163+
try {
164+
const pm = new PackageManager(adb);
165+
await pm.installStream(apkSize, new ReadableStream({
166+
start: (controller) => {
167+
controller.enqueue(apkBytes);
168+
controller.close();
169+
},
170+
}));
171+
logger.info(`[${serial}] Successfully installed APK '${apkPath}'`);
172+
return true;
173+
} catch (e) {
174+
logger.error(`[${serial}] Failed to install APK '${apkPath}': {e}`, { e });
175+
return false;
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)