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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions mobile/android/android-application-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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"]);
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 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;
Expand Down Expand Up @@ -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`);
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.

}
}
127 changes: 127 additions & 0 deletions test/unit-tests/android-application-manager.ts
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);
});
});
});