Skip to content
Open
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
30 changes: 29 additions & 1 deletion src/extension/android/packageNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ import { FileSystem } from "../../common/node/fileSystem";

export class PackageNameResolver {
private static PackageNameRegexp: RegExp = /package="(.+?)"/;
private static ApplicationIdRegexp: RegExp = /applicationId\s+(=)?\s*["'](.+?)["']/;
private static ManifestName = "AndroidManifest.xml";
private static GradleBuildName = "build.gradle";
private static DefaultPackagePrefix = "com.";
private static SourceRootRelPath: string[] = ["android", "app", "src", "main"];
private static DefaultManifestLocation: string[] = PackageNameResolver.SourceRootRelPath.concat(
PackageNameResolver.ManifestName,
);
private static DefaultGradleBuildLocation: string[] = [
"android",
"app",
PackageNameResolver.GradleBuildName,
];
private applicationName: string;

constructor(applicationName: string) {
Expand All @@ -22,14 +29,35 @@ export class PackageNameResolver {
* Tries to find the package name in AndroidManifest.xml. If not found, it returns the default package name,
* which is the application name prefixed with the default prefix.
*/
public resolvePackageName(projectRoot: string): Promise<string> {
public async resolvePackageName(projectRoot: string): Promise<string> {
const expectedGradleBuildPath = path.join.apply(
this,
[projectRoot].concat(PackageNameResolver.DefaultGradleBuildLocation),
);
const gradlePackageName = await this.readApplicationId(expectedGradleBuildPath);
if (gradlePackageName) {
return gradlePackageName;
}

const expectedAndroidManifestPath = path.join.apply(
this,
[projectRoot].concat(PackageNameResolver.DefaultManifestLocation),
);
return this.readPackageName(expectedAndroidManifestPath);
}

private async readApplicationId(gradlePath: string): Promise<string | null> {
if (gradlePath) {
const fs = new FileSystem();
if (await fs.exists(gradlePath)) {
const content = await fs.readFile(gradlePath);
const match = content.toString().match(PackageNameResolver.ApplicationIdRegexp);
return match ? match[2] : null;
}
}
return null;
}

/**
* Given a manifest file path, it parses the file and returns the package name.
* If the package name cannot be parsed, the default packge name is returned.
Expand Down
135 changes: 135 additions & 0 deletions test/extension/android/packageNameResolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

import * as path from "path";
import * as assert from "assert";
const sinon = require("sinon");
import { PackageNameResolver } from "../../../src/extension/android/packageNameResolver";
import { FileSystem } from "../../../src/common/node/fileSystem";

describe("PackageNameResolver", function () {
const projectRoot = path.join(__dirname, "mockProject");
const androidRoot = path.join(projectRoot, "android");
const appRoot = path.join(androidRoot, "app");
const manifestPath = path.join(appRoot, "src", "main", "AndroidManifest.xml");
const buildGradlePath = path.join(appRoot, "build.gradle");

let readFileStub: any;
let existsStub: any;

beforeEach(() => {
// Mock FileSystem methods
existsStub = sinon.stub(FileSystem.prototype, "exists");
readFileStub = sinon.stub(FileSystem.prototype, "readFile");
});

afterEach(() => {
if (existsStub) existsStub.restore();
if (readFileStub) readFileStub.restore();
});

it("should resolve package name from AndroidManifest.xml if build.gradle is missing", async () => {
const manifestContent = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.manifest">
</manifest>`;

existsStub.withArgs(manifestPath).returns(Promise.resolve(true));
existsStub.withArgs(buildGradlePath).returns(Promise.resolve(false));
readFileStub.withArgs(manifestPath).returns(Promise.resolve(manifestContent));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

assert.strictEqual(packageName, "com.example.manifest");
});

it("should resolve application id from build.gradle", async () => {
const buildGradleContent = `
android {
defaultConfig {
applicationId "com.example.gradle"
}
}
`;

existsStub.withArgs(buildGradlePath).returns(Promise.resolve(true));
readFileStub.withArgs(buildGradlePath).returns(Promise.resolve(buildGradleContent));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

// This is expected to fail until the fix is implemented
assert.strictEqual(packageName, "com.example.gradle");
});

it("should prioritize build.gradle applicationId over AndroidManifest.xml package", async () => {
const manifestContent = `
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.manifest">
</manifest>`;
const buildGradleContent = `
android {
defaultConfig {
applicationId "com.example.gradle"
}
}
`;

existsStub.withArgs(manifestPath).returns(Promise.resolve(true));
existsStub.withArgs(buildGradlePath).returns(Promise.resolve(true));
readFileStub.withArgs(manifestPath).returns(Promise.resolve(manifestContent));
readFileStub.withArgs(buildGradlePath).returns(Promise.resolve(buildGradleContent));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

// This is expected to fail until the fix is implemented
assert.strictEqual(packageName, "com.example.gradle");
});

it("should resolve application id from build.gradle using single quotes", async () => {
const buildGradleContent = `
android {
defaultConfig {
applicationId 'com.example.gradle.singlequote'
}
}
`;

existsStub.withArgs(buildGradlePath).returns(Promise.resolve(true));
readFileStub.withArgs(buildGradlePath).returns(Promise.resolve(buildGradleContent));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

assert.strictEqual(packageName, "com.example.gradle.singlequote");
});

it("should resolve application id from build.gradle using assignment", async () => {
const buildGradleContent = `
android {
defaultConfig {
applicationId = "com.example.gradle.assignment"
}
}
`;

existsStub.withArgs(buildGradlePath).returns(Promise.resolve(true));
readFileStub.withArgs(buildGradlePath).returns(Promise.resolve(buildGradleContent));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

assert.strictEqual(packageName, "com.example.gradle.assignment");
});

it("should fall back to default package name if neither file exists", async () => {
existsStub.returns(Promise.resolve(false));

const resolver = new PackageNameResolver("ExampleApp");
const packageName = await resolver.resolvePackageName(projectRoot);

assert.strictEqual(packageName, "com.exampleapp");
});
});
Loading