diff --git a/README.md b/README.md index 28ce306..6be4f1d 100644 --- a/README.md +++ b/README.md @@ -34,31 +34,61 @@ The following settings are used to configure the debugger: "version": "0.2.0", "configurations": [ { - // configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session + // configuration type, request and name. "launch" is used to deploy the app + // to your device and start a debugging session. "type": "android", "request": "launch", "name": "Launch App", - // Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml) + // Location of the App source files. This value must point to the root of + // your App source tree (containing AndroidManifest.xml). "appSrcRoot": "${workspaceRoot}/app/src/main", - // Fully qualified path to the built APK (Android Application Package) + // Fully qualified path to the built APK (Android Application Package). "apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk", - // Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037 + // Port number to connect to the local ADB (Android Debug Bridge) instance. + // Default: 5037 "adbPort": 5037, - // Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn + // Automatically launch 'adb start-server' if not already started. + // Default: true + "autoStartADB": true, + + // Launch behaviour if source files have been saved after the APK was built. + // One of: [ ignore warn stop ]. Default: warn "staleBuild": "warn", - // Fully qualified path to the AndroidManifest.xml file compiled in the APK. Default: appSrcRoot/AndroidManifest.xml + // Target Device ID (as indicated by 'adb devices'). + // Use this to specify which device is used for deployment + // when multiple devices are connected. + "targetDevice": "", + + // Fully qualified path to the AndroidManifest.xml file compiled into the APK. + // Default: "${appSrcRoot}/AndroidManifest.xml" "manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml", - // APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: ["-r"] + // Custom arguments passed to the Android package manager to install the app. + // Run 'adb shell pm' to show valid arguments. Default: ["-r"] "pmInstallArgs": ["-r"], - // Manually specify the activity to run when the app is started. - "launchActivity": ".MainActivity" + // Custom arguments passed to the Android application manager to start the app. + // Run `adb shell am` to show valid arguments. + // Note that `-D` is required to enable debugging. + "amStartArgs": [ + "-D", + "--activity-brought-to-front", + "-a android.intent.action.MAIN", + "-c android.intent.category.LAUNCHER", + "-n package.name/launch.activity" + ], + + // Manually specify the activity to run when the app is started. This option is + // mutually exclusive with "amStartArgs". + "launchActivity": ".MainActivity", + + // Set to true to output debugging logs for diagnostics. + "trace": false } ] } diff --git a/package.json b/package.json index f0cfb7d..d70f1b5 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,17 @@ "adbPort" ], "properties": { + "amStartArgs": { + "type": "array", + "description": "Custom arguments to pass to the Android application manager to start the app. Run `adb shell am` to show valid arguments. Note that `-D` is required to enable debugging.\r\nBe careful using this option - you must specify the correct parameters or the app will not start.\r\n\r\nThis option is incompatible with the `launchActivity` option.", + "default": [ + "-D", + "--activity-brought-to-front", + "-a android.intent.action.MAIN", + "-c android.intent.category.LAUNCHER", + "-n package.name/launch.activity" + ] + }, "appSrcRoot": { "type": "string", "description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)", diff --git a/src/debugMain.js b/src/debugMain.js index 86f43f2..d0e8072 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -53,6 +53,18 @@ class AndroidDebugSession extends DebugSession { // the full file path name of the AndroidManifest.xml, taken from the manifestFile launch property this.manifest_fpn = ''; + /** + * array of custom arguments to pass to `pm install` + * @type {string[]} + */ + this.pm_install_args = null; + + /** + * array of custom arguments to pass to `am start` + * @type {string[]} + */ + this.am_start_args = null; + /** * the threads (from the last refreshThreads() call) * @type {AndroidThread[]} @@ -263,10 +275,18 @@ class AndroidDebugSession extends DebugSession { this.app_src_root = ensure_path_end_slash(args.appSrcRoot); this.apk_fpn = args.apkFile; this.manifest_fpn = args.manifestFile; - this.pmInstallArgs = args.pmInstallArgs; + this.pm_install_args = args.pmInstallArgs; + this.am_start_args = args.amStartArgs; if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0) this.callStackDisplaySize = args.callStackDisplaySize|0; + // we don't allow both amStartArgs and launchActivity to be specified (the launch activity must be included in amStartArgs) + if (args.amStartArgs && args.launchActivity) { + this.LOG('amStartArgs and launchActivity options cannot both be specified in the launch configuration.'); + this.sendEvent(new TerminatedEvent(false)); + return; + } + // set the custom ADB port - this should be changed to pass it to each ADBClient instance if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) { ADBSocket.ADBPort = args.adbPort; @@ -378,16 +398,24 @@ class AndroidDebugSession extends DebugSession { } } - startLaunchActivity(launchActivity) { + async startLaunchActivity(launchActivity) { if (!launchActivity) { - if (!(launchActivity = this.apk_file_info.manifest.launcher)) { - throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json'); + // we're allowed no launchActivity if we have a custom am start command + if (!this.am_start_args) { + if (!(launchActivity = this.apk_file_info.manifest.launcher)) { + throw new Error('No valid launch activity found in AndroidManifest.xml or launch.json'); + } } } - const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity); - this.LOG(`Launching ${build.pkgname}/${launchActivity} on device ${this._device.serial} [API:${this.device_api_level||'?'}]`); - return this.dbgr.startDebugSession(build, this._device.serial); + const build = new BuildInfo(this.apk_file_info.manifest.package, new Map(this.src_packages.packages), launchActivity, this.am_start_args); + + this.LOG(`Launching on device ${this._device.serial} [API:${this.device_api_level||'?'}]`); + if (this.am_start_args) { + this.LOG(`Using custom launch arguments '${this.am_start_args.join(' ')}'`); + } + const am_stdout = await this.dbgr.startDebugSession(build, this._device.serial); + this.LOG(am_stdout); } async configureAPISourcePath() { @@ -440,7 +468,8 @@ class AndroidDebugSession extends DebugSession { }) // send the install command this.LOG('Installing...'); - const command = `pm install ${Array.isArray(this.pmInstallArgs) ? this.pmInstallArgs.join(' ') : '-r'} ${device_apk_fpn}`; + const pm_install_args = Array.isArray(this.pm_install_args) ? this.pm_install_args.join(' ') : '-r'; + const command = `pm install ${pm_install_args} ${device_apk_fpn}`; D(command); const stdout = await this._device.adbclient.shell_cmd({ command, diff --git a/src/debugger-types.js b/src/debugger-types.js index 359160b..d5ae11b 100644 --- a/src/debugger-types.js +++ b/src/debugger-types.js @@ -9,13 +9,14 @@ class BuildInfo { * @param {string} pkgname * @param {Map} packages * @param {string} launchActivity + * @param {string[]} amCommandArgs custom arguments passed to `am start` */ - constructor(pkgname, packages, launchActivity) { + constructor(pkgname, packages, launchActivity, amCommandArgs) { this.pkgname = pkgname; this.packages = packages; this.launchActivity = launchActivity; /** the arguments passed to `am start` */ - this.startCommandArgs = [ + this.startCommandArgs = amCommandArgs || [ '-D', // enable debugging '--activity-brought-to-front', '-a android.intent.action.MAIN', diff --git a/src/debugger.js b/src/debugger.js index e0b0cd8..23d15d4 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -29,7 +29,7 @@ const { TypeNotAvailable, } = require('./debugger-types'); -class Debugger extends EventEmitter { +class Debugger extends EventEmitter { constructor () { super(); @@ -76,7 +76,7 @@ class Debugger extends EventEmitter { */ async startDebugSession(build, deviceid) { this.session = new DebugSession(build, deviceid); - await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause); + const stdout = await Debugger.runApp(deviceid, build.startCommandArgs, build.postLaunchPause); // retrieve the list of debuggable processes const pids = await this.getDebuggablePIDs(this.session.deviceid); @@ -84,6 +84,7 @@ class Debugger extends EventEmitter { const pid = pids[pids.length - 1]; // after connect(), the caller must call resume() to begin await this.connect(pid); + return stdout; } /** @@ -94,7 +95,7 @@ class Debugger extends EventEmitter { static async runApp(deviceid, launch_cmd_args, post_launch_pause = 1000) { // older (<3) versions of Android only allow target components to be specified with -n const shell_cmd = { - command: 'am start ' + launch_cmd_args.join(' '), + command: `am start ${launch_cmd_args.join(' ')}`, }; let retries = 10 for (;;) { @@ -104,12 +105,14 @@ class Debugger extends EventEmitter { await sleep(post_launch_pause); // failures: // Error: Activity not started... - const m = stdout.match(/Error:.*/g); + // /system/bin/sh: syntax error: unexpected EOF - this happens with invalid am command arguments + const m = stdout.match(/Error:.*|syntax error:/gi); if (!m) { - break; + // return the stdout from am (it shows the fully qualified component name) + return stdout.toString().trim(); } else if (retries <= 0){ - throw new Error(m[0]); + throw new Error(stdout.toString().trim()); } retries -= 1; }