diff --git a/src/extension/android/packageNameResolver.ts b/src/extension/android/packageNameResolver.ts index 80cf6861c..bfb458514 100644 --- a/src/extension/android/packageNameResolver.ts +++ b/src/extension/android/packageNameResolver.ts @@ -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) { @@ -22,7 +29,16 @@ 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 { + public async resolvePackageName(projectRoot: string): Promise { + 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), @@ -30,6 +46,18 @@ export class PackageNameResolver { return this.readPackageName(expectedAndroidManifestPath); } + private async readApplicationId(gradlePath: string): Promise { + 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. diff --git a/test/extension/android/packageNameResolver.test.ts b/test/extension/android/packageNameResolver.test.ts new file mode 100644 index 000000000..54404bc4d --- /dev/null +++ b/test/extension/android/packageNameResolver.test.ts @@ -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"; + +suite("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; + + setup(() => { + // Mock FileSystem methods + existsStub = sinon.stub(FileSystem.prototype, "exists"); + readFileStub = sinon.stub(FileSystem.prototype, "readFile"); + }); + + teardown(() => { + if (existsStub) existsStub.restore(); + if (readFileStub) readFileStub.restore(); + }); + + test("should resolve package name from AndroidManifest.xml if build.gradle is missing", async () => { + const manifestContent = ` + + `; + + 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"); + }); + + test("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"); + }); + + test("should prioritize build.gradle applicationId over AndroidManifest.xml package", async () => { + const manifestContent = ` + + `; + 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"); + }); + + test("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"); + }); + + test("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"); + }); + + test("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"); + }); +});