Skip to content
This repository was archived by the owner on Feb 2, 2021. It is now read-only.

use am start instead of monkey to start application #995

Merged
merged 7 commits into from
Aug 11, 2017

Conversation

Plamen5kov
Copy link
Contributor

@Plamen5kov Plamen5kov commented Jul 31, 2017

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 replacing monkey with the recommended am 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

@Plamen5kov Plamen5kov force-pushed the plamen5kov/use-am-instead-of-monkey branch from 4431195 to 74a0ec2 Compare July 31, 2017 08:55
@Plamen5kov Plamen5kov self-assigned this Jul 31, 2017
Copy link
Contributor

@TsvetanMilanov TsvetanMilanov left a 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_])*/;
Copy link
Contributor

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?

Copy link
Collaborator

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 :)

Copy link
Contributor Author

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!

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

@Plamen5kov Plamen5kov Aug 2, 2017

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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`);
Copy link
Contributor

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.

Copy link
Contributor Author

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

@Plamen5kov
Copy link
Contributor Author

Plamen5kov commented Aug 3, 2017

@TsvetanMilanov the method you're referring to runApplicationOnEmulatorCore in the android-emulator-services is not used since we removed the emulate command. We can remove the whole emulator service, but it let it be in another PR.

@Plamen5kov Plamen5kov force-pushed the plamen5kov/use-am-instead-of-monkey branch from eea4ae4 to c00d54a Compare August 3, 2017 12:07
@@ -102,4 +114,8 @@ export class AndroidApplicationManager extends ApplicationManagerBase {

return applicationViews;
}

public getFullyQualifiedActivityRegex(): RegExp {
Copy link
Contributor

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");
Copy link
Contributor

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.

Copy link
Contributor Author

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"]);
Copy link
Contributor

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_])*/;
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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);?

Copy link
Contributor Author

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 = [
Copy link
Contributor

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", () => {
Copy link
Contributor

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>,
Copy link
Contributor

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>;
Copy link
Contributor

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[]

Copy link
Contributor Author

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?

Copy link
Contributor

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 () => {
Copy link
Contributor

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.

@TsvetanMilanov
Copy link
Contributor

@Plamen5kov Can you check if androidEmulatorServices is used in the AppBuilder CLI before you remove it?

@Plamen5kov
Copy link
Contributor Author

Sure, when I make another PR about removing the androidEmulatorService, I'll check.

Copy link
Collaborator

@rosen-vladimirov rosen-vladimirov left a 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`);
Copy link
Collaborator

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>

Copy link
Contributor Author

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"]);
Copy link
Collaborator

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?

Copy link
Collaborator

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

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 = "";
Copy link
Collaborator

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();
Copy link
Collaborator

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) {
Copy link
Collaborator

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];
Copy link
Collaborator

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i +=1 -> i++

Copy link
Contributor Author

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Collaborator

@rosen-vladimirov rosen-vladimirov left a 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"]);
Copy link
Collaborator

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");
Copy link
Collaborator

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);
Copy link
Collaborator

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;

@Plamen5kov Plamen5kov force-pushed the plamen5kov/use-am-instead-of-monkey branch from bcaaef0 to c5557e0 Compare August 11, 2017 05:16
@Plamen5kov
Copy link
Contributor Author

Plamen5kov commented Aug 11, 2017

ping @TsvetanMilanov please tell me if you have any further suggestions

@Plamen5kov Plamen5kov force-pushed the plamen5kov/use-am-instead-of-monkey branch from c5557e0 to e92d53e Compare August 11, 2017 14:02
@Plamen5kov Plamen5kov merged commit 99ae8cc into master Aug 11, 2017
@Plamen5kov Plamen5kov deleted the plamen5kov/use-am-instead-of-monkey branch August 11, 2017 14:09
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants