-
Notifications
You must be signed in to change notification settings - Fork 10
use am start instead of monkey to start application #995
Conversation
4431195
to
74a0ec2
Compare
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.
The monkey
tool is also used in https://github.com/telerik/mobile-cli-lib/blob/master/mobile/android/android-emulator-services.ts#L181. I think you should change it there as well.
"-c", "android.intent.category.LAUNCHER", | ||
"1"]); | ||
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const fullActivityNameRegExp = /([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*\/([a-z][a-z_0-9]*\.)*[A-Z_]($[A-Z_]|[\w_])*/; |
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.
Can you provide some example output of pm dump
?
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.
Also this regex is really complicated. Is it from official documentation? In any case we need tests for it :)
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.
One regex is for verified Android application package and the second part is for a fully qualified java class name. Will write the tests!
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.
Here's an example of the pm dump | grep -A 1 MAIN
:
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}
and the regex finds the full path to the activity name, which in this case is: org.nativescript.cliapp/com.tns.NativeScriptActivity
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const fullActivityNameRegExp = /([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*\/([a-z][a-z_0-9]*\.)*[A-Z_]($[A-Z_]|[\w_])*/; | ||
const activityMatch = new RegExp(fullActivityNameRegExp, "m"); | ||
const possibleIdentifier = activityMatch.exec(pmDumpOutput)[0]; |
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.
activityMatch.exec(pmDumpOutput)
will return null
if nothing matches the RegExp. I think it will be better to check the result before we try to get the first match from it.
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.
will do!
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const fullActivityNameRegExp = /([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*\/([a-z][a-z_0-9]*\.)*[A-Z_]($[A-Z_]|[\w_])*/; | ||
const activityMatch = new RegExp(fullActivityNameRegExp, "m"); | ||
const possibleIdentifier = activityMatch.exec(pmDumpOutput)[0]; |
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.
Also I think if the result is null
we should try to start the default NativeScript/AppBuilder activity. This way if the output from pm dump
is changed we will be able to start applications which use the default activity.
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.
I'd prefer we don't start Activities that are not classified with the "MAIN" tag. The activity may be in the manifest, but not as a "MAIN" activity, and it will be an unexpected behavior to start an activity not meant as main.
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.
Yes, it will be unexpected behavior. My point is that if we start the default main activity, after we have failed to get it from pm dump, we will not break the CLI for the users who use the default main activity.
When we use the monkey tool we know that it will start the main activity, no matter how it's named. With the new logic we search for the main activity name with grep -A 1
and I think it is possible that on different Android versions the output can be different. Also if we hit other problems with the new approach we will not be able to start any application if we don't use the default main activity.
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.
I understand your point, but we can't start a hardcoded activity and hope it's the main one in the manifest.
if (possibleIdentifier) { | ||
await this.adb.executeShellCommand(["am", "start", "-n", possibleIdentifier]); | ||
} else { | ||
this.$logger.trace(`Tried starting activity: ${possibleIdentifier}, but failed`); |
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.
We will get here if we don't have possibleIdentifier
. This means that we will log null/undefined/""
. Maybe we should log the appIdentifier
.
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.
good catch will fix to show app identifier
@TsvetanMilanov the method you're referring to |
eea4ae4
to
c00d54a
Compare
@@ -102,4 +114,8 @@ export class AndroidApplicationManager extends ApplicationManagerBase { | |||
|
|||
return applicationViews; | |||
} | |||
|
|||
public getFullyQualifiedActivityRegex(): RegExp { |
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.
This method is used only in this class. It should be private
. You should test it through the public method startApplication
.
"1"]); | ||
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const fullActivityNameRegExp = this.getFullyQualifiedActivityRegex(); | ||
const activityMatch = new RegExp(fullActivityNameRegExp, "m"); |
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.
fullActivityNameRegExp
is RegExp. You don't need to create new RegExp from RegExp. Just add the m
flag in getFullyQualifiedActivityRegex
.
Also I don't think you need the m
flag.
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.
I do need the "m" flag because we are searching for "MAIN", but the actual string we're matching is on the next line.
"-p", appIdentifier, | ||
"-c", "android.intent.category.LAUNCHER", | ||
"1"]); | ||
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); |
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.
You should add the example output from pm dump
from #995 (comment) as comment here.
@@ -102,4 +114,8 @@ export class AndroidApplicationManager extends ApplicationManagerBase { | |||
|
|||
return applicationViews; | |||
} | |||
|
|||
public getFullyQualifiedActivityRegex(): RegExp { | |||
return /([A-Za-z]{1}[A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*\/([a-z][a-z_0-9]*\.)*[A-Z_$]($[A-Z_$]|[$_\w_])*/; |
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.
If you want to make this long regular expression easier to read/understand you can make this method like this one -> https://github.com/telerik/mobile-cli-lib/blob/master/mobile/mobile-core/android-process-service.ts#L18.
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.
I didn't want to split the regex because of the escaping, but if it's done like so in other places, I can also do it here.
assert.isArray(match); | ||
assert.isTrue(expectedElement === match[0]); | ||
} | ||
assert.isTrue(true); |
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.
Why we need this assert.isTrue(true);
?
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.
must have forgotten it when testing, will remove it
"org.nativescript.testApp/com.tns.$_TestClass", | ||
"org.nativescript.testApp/com.tns._$TestClass" | ||
], | ||
validTestInput = [ |
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.
You should add more test cases which include some real output from the pm dump
command.
testInjector.register("deviceLogProvider", {}); | ||
testInjector.register("hooksService", {}); | ||
}); | ||
describe("tries to get fully qualified activity class name", () => { |
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.
This describe should be the name of the method it tests. This way it will be consistent with the other test files. Example -> https://github.com/telerik/mobile-cli-lib/blob/master/test/unit-tests/analytics-service.ts#L173
describe("android-application-manager", () => { | ||
|
||
let testInjector: IInjector, | ||
validTestInput: Array<string>, |
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.
I think this variable should be in the test which uses it.
Also it's type should be string[]
|
||
let testInjector: IInjector, | ||
validTestInput: Array<string>, | ||
expectedValidTestInput: Array<string>; |
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.
I think this variable should be in the test which uses it.
Also it's type should be string[]
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.
why does it matter if they are string[] or Array?
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.
There is no difference between Array<string>
and string[]
. I prefer to use string[]
for consistency with the other code.
testInjector.register("hooksService", {}); | ||
}); | ||
describe("tries to get fully qualified activity class name", () => { | ||
it("and succeeds finding the right name", async () => { |
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.
Please add test which verifies that we default to monkey if we can't get the main activity name.
@Plamen5kov Can you check if |
Sure, when I make another PR about removing the androidEmulatorService, I'll check. |
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.
I have some concerns regarding usage of pm dump
. I'll need at least clarification of some cases before approving this PR.
I like the tests.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Creating RegExp every time is slow operation. You can add @cache
decorator at the top of the method.
However, I'm wondering why can't we apply specific regular expression for each application identifier that we are using. At the moment, the regular expression is generic one, but wouldn't it be easier (and safer) to use the applicationIdentifier in the regular expression. In fact we are searching for <application identifier>/<fullJavaClassName>
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.
Will put @cache
as you suggested, thank you!
I thought about using the app identifier for the first part of the regex, but I prefer to keep the general implementation since the app identifier is sometimes overridden by the user in app.gradle
.
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"]); |
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:
DUMP OF SERVICE package:
Activity Resolver Table:
Non-Data Actions:
android.intent.action.MAIN:
17f3e44f org.nativescript.appwithuniquename/com.tns.NativeScriptActivity
268277dc org.nativescript.appwithuniquename/com.tns.NativeScriptActivity1
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:
<activity
android:name="com.tns.NativeScriptActivity"
android:label="@string/title_activity_kimera"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/LaunchScreenTheme">
<meta-data android:name="SET_THEME_ON_LAUNCH" android:resource="@style/AppTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.tns.NativeScriptActivity1">
<meta-data android:name="Test" android:resource="@style/AppTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
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, is grep -A
available on all Android devices? Also we are getting part of the output of pm 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 of dumpsys
because when I saw the content of pm
it seemed more reliable because it was part of the framework.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.
generic_x86:/ # cat /system/bin/pm
# Script to start "pm" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/pm.jar
exec app_process $base/bin com.android.commands.pm.Pm "$@"
generic_x86:/ # cat /system/bin/am
#!/system/bin/sh
#
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
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.
const pmDumpOutput = await this.adb.executeShellCommand(["pm", "dump", appIdentifier, "|", "grep", "-A", "1", "MAIN"]); | ||
const activityMatch = this.getFullyQualifiedActivityRegex(); | ||
const match = activityMatch.exec(pmDumpOutput); | ||
let possibleIdentifier = ""; |
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.
this can be:
const possibleIdentifier = match && match[0];
} | ||
} | ||
AndroidDebugBridgeStub.methodCallCount++; | ||
Promise.resolve(); |
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.
you do not need this
if (args && args.length) { | ||
const possibleIdentifier = args[args.length - 1]; | ||
let validTestString = this.expectedValidTestInput[AndroidDebugBridgeStub.methodCallCount]; | ||
if (possibleIdentifier === validTestString) { |
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.
return possibleIdentifier === validTestString;
private checkIfValidIdentifierPassed(args: string[]): Boolean { | ||
if (args && args.length) { | ||
const possibleIdentifier = args[args.length - 1]; | ||
let validTestString = this.expectedValidTestInput[AndroidDebugBridgeStub.methodCallCount]; |
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.
const
}); | ||
describe("startApplication", () => { | ||
it("fires up the right application", async () => { | ||
for (let i = 0; i < androidDebugBridge.getInputLength(); i += 1) { |
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.
i +=1
-> i++
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.
i += 1
is faster, but if you insist I have no problem.
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.
Well, that's not entirely correct. Consider the following example.
const iterations = 100*1000*1000;
console.time("i+=1");
for(let i = 0; i < iterations; i+=1) {
}
console.timeEnd("i+=1");
console.time("i++");
for(let i = 0; i < iterations; i++) {
}
console.timeEnd("i++");
And the result is:
$ node b.js
i+=1: 1118.667ms
i++: 887.539ms
$ node b.js
i+=1: 1116.609ms
i++: 877.461ms
$ node b.js
i+=1: 1106.636ms
i++: 882.537ms
But anyway, the codebase contains many lines with <variable name>++
, so I prefer consistency 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.
Ha, cool they fixed it. Will change it.
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.
After fixing comments and green build.
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"]); |
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.
if (args[0] === "pm") { | ||
const passedIdentifier = args[2]; | ||
if (passedIdentifier === invalidIdentifier) { | ||
return Promise.resolve("invalid output string"); |
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.
No need for Promise.resolve
here - just return "invalid output string";
return Promise.resolve("invalid output string"); | ||
} else { | ||
const testString = this.validTestInput[AndroidDebugBridgeStub.methodCallCount]; | ||
return Promise.resolve(testString); |
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.
No need for Promise.resolve
here - just return testString;
bcaaef0
to
c5557e0
Compare
ping @TsvetanMilanov please tell me if you have any further suggestions |
split regex added fallback to monkey test refactored tests
c5557e0
to
e92d53e
Compare
problem
monkey
is a tool used for testing, not for production use and we shouldn't use it since it's a tool used and built for stress testing. It has random behavior and it's not recommended for production use and therefore we are replacingmonkey
with the recommendedam
tool.Fix Issue: NativeScript/NativeScript#4591
solution
Use
am
tool instead to start the application by it's activity identifier which is a combination of the package name and the default activity.Example for such an identifier would be:
org.nativescript.appname/com.tns.MainActivity