-
Notifications
You must be signed in to change notification settings - Fork 10
use am start instead of monkey to start application #995
Changes from all commits
fcb12ba
7cd02d5
991db6e
4738607
bc034c7
8b4b8fa
e92d53e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ import { EOL } from "os"; | |
import { ApplicationManagerBase } from "../application-manager-base"; | ||
import { LiveSyncConstants, TARGET_FRAMEWORK_IDENTIFIERS } from "../../constants"; | ||
import { hook } from "../../helpers"; | ||
import { cache } from "../../decorators"; | ||
|
||
export class AndroidApplicationManager extends ApplicationManagerBase { | ||
|
||
|
@@ -44,10 +45,31 @@ export class AndroidApplicationManager extends ApplicationManagerBase { | |
} | ||
|
||
public async startApplication(appIdentifier: string): Promise<void> { | ||
await this.adb.executeShellCommand(["monkey", | ||
"-p", appIdentifier, | ||
"-c", "android.intent.category.LAUNCHER", | ||
"1"]); | ||
|
||
/* | ||
Example "pm dump <app_identifier> | grep -A 1 MAIN" output" | ||
android.intent.action.MAIN: | ||
3b2df03 org.nativescript.cliapp/com.tns.NativeScriptActivity filter 50dd82e | ||
Action: "android.intent.action.MAIN" | ||
Category: "android.intent.category.LAUNCHER" | ||
-- | ||
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.nativescript.cliapp/com.tns.NativeScriptActivity} | ||
realActivity=org.nativescript.cliapp/com.tns.NativeScriptActivity | ||
-- | ||
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.nativescript.cliapp/com.tns.NativeScriptActivity } | ||
frontOfTask=true task=TaskRecord{fe592ac #449 A=org.nativescript.cliapp U=0 StackId=1 sz=1} | ||
*/ | ||
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const activityMatch = this.getFullyQualifiedActivityRegex(); | ||
const match = activityMatch.exec(pmDumpOutput); | ||
const possibleIdentifier = match && match[0]; | ||
|
||
if (possibleIdentifier) { | ||
await this.adb.executeShellCommand(["am", "start", "-n", possibleIdentifier]); | ||
} else { | ||
this.$logger.trace(`Tried starting activity for: ${appIdentifier}, using activity manager but failed.`); | ||
await this.adb.executeShellCommand(["monkey", "-p", appIdentifier, "-c", "android.intent.category.LAUNCHER", "1"]); | ||
} | ||
|
||
if (!this.$options.justlaunch) { | ||
const deviceIdentifier = this.identifier; | ||
|
@@ -102,4 +124,13 @@ export class AndroidApplicationManager extends ApplicationManagerBase { | |
|
||
return applicationViews; | ||
} | ||
|
||
@cache() | ||
private getFullyQualifiedActivityRegex(): RegExp { | ||
const androidPackageName = "([A-Za-z]{1}[A-Za-z\\d_]*\\.)*[A-Za-z][A-Za-z\\d_]*"; | ||
const packageActivitySeparator = "\\/"; | ||
const fullJavaClassName = "([a-z][a-z_0-9]*\\.)*[A-Z_$]($[A-Z_$]|[$_\\w_])*"; | ||
|
||
return new RegExp(`${androidPackageName}${packageActivitySeparator}${fullJavaClassName}`, `m`); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Creating RegExp every time is slow operation. You can add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will put |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { AndroidApplicationManager } from "../../mobile/android/android-application-manager"; | ||
import { Yok } from "../../yok"; | ||
import { assert } from "chai"; | ||
import { CommonLoggerStub } from "./stubs"; | ||
const invalidIdentifier: string = "invalid.identifier"; | ||
|
||
class AndroidDebugBridgeStub { | ||
public startedWithActivityManager: Boolean = false; | ||
public validIdentifierPassed: Boolean = false; | ||
public static methodCallCount: number = 0; | ||
private expectedValidTestInput: string[] = [ | ||
"org.nativescript.testApp/com.tns.TestClass", | ||
"org.nativescript.testApp/com.tns.$TestClass", | ||
"org.nativescript.testApp/com.tns._TestClass", | ||
"org.nativescript.testApp/com.tns.$_TestClass", | ||
"org.nativescript.testApp/com.tns._$TestClass", | ||
"org.nativescript.testApp/com.tns.NativeScriptActivity" | ||
]; | ||
private validTestInput: string[] = [ | ||
"other.stuff/ org.nativescript.testApp/com.tns.TestClass asdaas.dasdh2", | ||
"other.stuff.the.regex.might.fail.on org.nativescript.testApp/com.tns.$TestClass other.stuff.the.regex.might.fail.on", | ||
"/might.fail.on org.nativescript.testApp/com.tns._TestClass /might.fail.on", | ||
"might.fail.on/ org.nativescript.testApp/com.tns.$_TestClass might.fail.on//", | ||
"/might.fail org.nativescript.testApp/com.tns._$TestClass something/might.fail.on/", | ||
"android.intent.action.MAIN: \ | ||
3b2df03 org.nativescript.testApp/com.tns.NativeScriptActivity filter 50dd82e \ | ||
Action: \"android.intent.action.MAIN\" \ | ||
Category: \"android.intent.category.LAUNCHER\" \ | ||
-- \ | ||
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.nativescript.testApp/com.tns.NativeScriptActivity} \ | ||
realActivity=org.nativescript.testApp/com.tns.NativeScriptActivity \ | ||
-- \ | ||
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.nativescript.testApp/com.tns.NativeScriptActivity } \ | ||
frontOfTask=true task=TaskRecord{fe592ac #449 A=org.nativescript.testApp U=0 StackId=1 sz=1}" | ||
]; | ||
|
||
public async executeShellCommand(args: string[]): Promise<any> { | ||
if (args && args.length > 0) { | ||
if (args[0] === "pm") { | ||
const passedIdentifier = args[2]; | ||
if (passedIdentifier === invalidIdentifier) { | ||
return "invalid output string"; | ||
} else { | ||
const testString = this.validTestInput[AndroidDebugBridgeStub.methodCallCount]; | ||
return testString; | ||
} | ||
} else { | ||
this.startedWithActivityManager = this.checkIfStartedWithActivityManager(args); | ||
if (this.startedWithActivityManager) { | ||
this.validIdentifierPassed = this.checkIfValidIdentifierPassed(args); | ||
} | ||
} | ||
} | ||
AndroidDebugBridgeStub.methodCallCount++; | ||
} | ||
|
||
public getInputLength(): number { | ||
return this.validTestInput.length; | ||
} | ||
|
||
private checkIfStartedWithActivityManager(args: string[]): Boolean { | ||
const firstArgument = args[0].trim(); | ||
switch (firstArgument) { | ||
case "am": return true; | ||
case "monkey": return false; | ||
default: return false; | ||
} | ||
} | ||
|
||
private checkIfValidIdentifierPassed(args: string[]): Boolean { | ||
if (args && args.length) { | ||
const possibleIdentifier = args[args.length - 1]; | ||
const validTestString = this.expectedValidTestInput[AndroidDebugBridgeStub.methodCallCount]; | ||
|
||
return possibleIdentifier === validTestString; | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
function createTestInjector(): IInjector { | ||
let testInjector = new Yok(); | ||
testInjector.register("androidApplicationManager", AndroidApplicationManager); | ||
testInjector.register("adb", AndroidDebugBridgeStub); | ||
testInjector.register('childProcess', {}); | ||
testInjector.register("logger", CommonLoggerStub); | ||
testInjector.register("config", {}); | ||
testInjector.register("staticConfig", {}); | ||
testInjector.register("androidDebugBridgeResultHandler", {}); | ||
testInjector.register("options", {justlaunch: true}); | ||
testInjector.register("errors", {}); | ||
testInjector.register("identifier", {}); | ||
testInjector.register("logcatHelper", {}); | ||
testInjector.register("androidProcessService", {}); | ||
testInjector.register("httpClient", {}); | ||
testInjector.register("deviceLogProvider", {}); | ||
testInjector.register("hooksService", {}); | ||
return testInjector; | ||
} | ||
|
||
describe("android-application-manager", () => { | ||
|
||
let testInjector: IInjector, | ||
androidApplicationManager:AndroidApplicationManager, | ||
androidDebugBridge:AndroidDebugBridgeStub; | ||
|
||
beforeEach(() => { | ||
testInjector = createTestInjector(); | ||
androidApplicationManager = testInjector.resolve("androidApplicationManager"); | ||
androidDebugBridge = testInjector.resolve("adb"); | ||
}); | ||
describe("startApplication", () => { | ||
it("fires up the right application", async () => { | ||
for (let i = 0; i < androidDebugBridge.getInputLength(); i++) { | ||
androidDebugBridge.validIdentifierPassed = false; | ||
|
||
await androidApplicationManager.startApplication("valid.identifier"); | ||
assert.isTrue(androidDebugBridge.validIdentifierPassed); | ||
assert.isTrue(androidDebugBridge.startedWithActivityManager); | ||
} | ||
}); | ||
it("if regex fails monkey is called to start application", async () => { | ||
await androidApplicationManager.startApplication(invalidIdentifier); | ||
assert.isFalse(androidDebugBridge.startedWithActivityManager); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what's the expected behavior when the application has more than one MAIN activity, for example:
Currently we'll get only the first one, are we fine with this?
Also, shouldn't we find the activity with Launcher category? For example in case I have the following in my manifest:
The
com.tns.NativeScriptActivity
should be started, will this happen?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it faster to call
adb shell dumpsys package <appIdentifier>
. On my machine it is twice faster, but maybe it's only here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Minor] Do we really need this
grep
? First of all, isgrep -A
available on all Android devices? Also we are getting part of the output ofpm dump
here and later we are searching for something via regular expression. I'm wondering, is it possible to make the regular expression smart enough to parse the full result.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what's the expected behavior when the application has more than one MAIN activity
The first main activity will be taken. I've not seen a user case where there's more than one MAIN activity, plus the user can specify a DEFAULT main activity, but I haven't seen that either, so I didn't want to complicate the implementation if there's no need.
Also, shouldn't we find the activity with Launcher category?
I also thought about this, but then I found out that some part of the usual Android applications don't specify a launcher category. Read more here. Keeping this in mind I thought best not to discriminate against such apps.
Regarding the example of the two MAIN activities you provided, I haven't thought about it, because up until now, I hadn't seen this scenario, but I'm open to discussing all possible scenarios.
Isn't it faster to call adb shell dumpsys package <appIdentifier>
To be honest I used
pm dump
instead ofdumpsys
because when I saw the content ofpm
it seemed more reliable because it was part of theframework.jar
.dumpsys
command is just a binary that is somewhere in the/system/bin
folder, and I couldn't find any information on its support and reliability.Do we really need this grep
I've tested this and the regex will work with the full result of the
pm dump
in case-A 1
isn't available, but the regex works faster in a shorter amount of text so I left it with the-A 1
option.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Plamen5kov , thanks for the detailed explanations. I'm not familiar with Android apps with more than one MAIN activities, there are such samples in Android Studio. It looks like the only way to understand if the current approach is fine is to merge it and see if such case will ever arise.