Skip to main content
Glama
prefs-reader.ts6.45 kB
/** * Android SharedPreferences Reader * Reads app preferences from device via adb */ import { ShellExecutor, defaultShellExecutor } from '../../utils/shell-executor.js'; import { PreferencesFile, PreferenceEntry, parseSharedPreferencesXml, } from '../../models/app-state.js'; /** * Options for reading preferences */ export interface ReadPreferencesOptions { /** Device ID */ deviceId?: string; /** Specific preferences file name */ fileName?: string; /** Timeout in milliseconds */ timeoutMs?: number; /** Shell executor for dependency injection */ shell?: ShellExecutor; } /** * Read all SharedPreferences files for an app * @param packageName Android package name * @param options Read options including optional shell executor */ export async function readSharedPreferences( packageName: string, options: ReadPreferencesOptions = {} ): Promise<PreferencesFile[]> { const { deviceId, fileName, timeoutMs = 10000, shell = defaultShellExecutor } = options; const prefsDir = `/data/data/${packageName}/shared_prefs`; // List preferences files const files = await listPreferencesFiles(packageName, deviceId, timeoutMs, shell); if (files.length === 0) { return []; } // Filter to specific file if requested const targetFiles = fileName ? files.filter((f) => f === fileName || f === `${fileName}.xml`) : files; // Read each preferences file const results: PreferencesFile[] = []; for (const file of targetFiles) { const filePath = `${prefsDir}/${file}`; const content = await readPreferencesFile(filePath, deviceId, timeoutMs, shell); if (content) { const entries = parseSharedPreferencesXml(content); results.push({ name: file.replace('.xml', ''), path: filePath, entries, }); } } return results; } /** * List SharedPreferences files for an app */ async function listPreferencesFiles( packageName: string, deviceId: string | undefined, timeoutMs: number, shell: ShellExecutor ): Promise<string[]> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } const prefsDir = `/data/data/${packageName}/shared_prefs`; args.push('shell', 'run-as', packageName, 'ls', prefsDir); try { const result = await shell.execute('adb', args, { timeoutMs }); if (result.exitCode !== 0) { // Try alternative method for debuggable apps return await listPreferencesFilesAlt(packageName, deviceId, timeoutMs, shell); } return result.stdout .split('\n') .map((f) => f.trim()) .filter((f) => f.endsWith('.xml')); } catch { return []; } } /** * Alternative method to list preferences files (for rooted devices or debuggable apps) */ async function listPreferencesFilesAlt( packageName: string, deviceId: string | undefined, timeoutMs: number, shell: ShellExecutor ): Promise<string[]> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } const prefsDir = `/data/data/${packageName}/shared_prefs`; args.push('shell', 'su', '-c', `ls ${prefsDir}`); try { const result = await shell.execute('adb', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } return result.stdout .split('\n') .map((f) => f.trim()) .filter((f) => f.endsWith('.xml')); } catch { return []; } } /** * Read a single preferences file */ async function readPreferencesFile( filePath: string, deviceId: string | undefined, timeoutMs: number, shell: ShellExecutor ): Promise<string | null> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } // Extract package name from path const packageMatch = filePath.match(/\/data\/data\/([^/]+)\//); const packageName = packageMatch ? packageMatch[1] : ''; if (!packageName) { return null; } // Use run-as to read the file args.push('shell', 'run-as', packageName, 'cat', filePath); try { const result = await shell.execute('adb', args, { timeoutMs }); if (result.exitCode !== 0) { // Try alternative method return await readPreferencesFileAlt(filePath, deviceId, timeoutMs, shell); } return result.stdout; } catch { return null; } } /** * Alternative method to read preferences file */ async function readPreferencesFileAlt( filePath: string, deviceId: string | undefined, timeoutMs: number, shell: ShellExecutor ): Promise<string | null> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('shell', 'su', '-c', `cat "${filePath}"`); try { const result = await shell.execute('adb', args, { timeoutMs }); return result.exitCode === 0 ? result.stdout : null; } catch { return null; } } /** * Read a specific preference value * @param packageName Android package name * @param prefsFileName Preferences file name * @param key Key to read * @param options Options including optional shell executor */ export async function readPreference( packageName: string, prefsFileName: string, key: string, options: { deviceId?: string; timeoutMs?: number; shell?: ShellExecutor } = {} ): Promise<PreferenceEntry | null> { const prefs = await readSharedPreferences(packageName, { ...options, fileName: prefsFileName, }); if (prefs.length === 0) { return null; } const entry = prefs[0].entries.find((e) => e.key === key); return entry || null; } /** * Check if app is debuggable (run-as accessible) * @param packageName Android package name * @param deviceId Device ID * @param shell Shell executor for dependency injection (defaults to real shell) */ export async function isAppDebuggable( packageName: string, deviceId?: string, shell: ShellExecutor = defaultShellExecutor ): Promise<boolean> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('shell', 'run-as', packageName, 'id'); try { const result = await shell.execute('adb', args, { timeoutMs: 5000 }); return result.exitCode === 0; } catch { return false; } } /** * Get app data directory path */ export function getAppDataPath(packageName: string): string { return `/data/data/${packageName}`; } /** * Get shared preferences directory path */ export function getSharedPrefsPath(packageName: string): string { return `/data/data/${packageName}/shared_prefs`; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server