Skip to content

Commit e0b4353

Browse files
committed
feat: support aab in run and deploy
1 parent 3627814 commit e0b4353

28 files changed

+1264
-628
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ $injector.require("androidPluginBuildService", "./services/android-plugin-build-
1414
$injector.require("gradleCommandService", "./services/android/gradle-command-service");
1515
$injector.require("gradleBuildService", "./services/android/gradle-build-service");
1616
$injector.require("gradleBuildArgsService", "./services/android/gradle-build-args-service");
17+
$injector.require("androidBundleToolService", "./services/android/android-bundle-tool-service");
1718
$injector.require("iOSEntitlementsService", "./services/ios-entitlements-service");
1819
$injector.require("iOSNativeTargetService", "./services/ios-native-target-service");
1920
$injector.require("iOSExtensionsService", "./services/ios-extensions-service");

lib/commands/deploy.ts

-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class DeployOnDeviceCommand extends ValidatePlatformCommandBase implement
1818
private $mobileHelper: Mobile.IMobileHelper,
1919
$platformsDataService: IPlatformsDataService,
2020
private $deployCommandHelper: DeployCommandHelper,
21-
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
2221
private $markingModeService: IMarkingModeService,
2322
private $migrateController: IMigrateController) {
2423
super($options, $platformsDataService, $platformValidationService, $projectData);
@@ -40,7 +39,6 @@ export class DeployOnDeviceCommand extends ValidatePlatformCommandBase implement
4039
await this.$migrateController.validate({ projectDir: this.$projectData.projectDir, platforms: [platform] });
4140
}
4241

43-
this.$androidBundleValidatorHelper.validateNoAab();
4442
if (!args || !args.length || args.length > 1) {
4543
return false;
4644
}

lib/commands/run.ts

-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export class RunCommandBase implements ICommand {
88
public platform: string;
99
constructor(
1010
private $analyticsService: IAnalyticsService,
11-
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
1211
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1312
private $errors: IErrors,
1413
private $hostInfo: IHostInfo,
@@ -34,7 +33,6 @@ export class RunCommandBase implements ICommand {
3433
this.platform = this.$devicePlatformsConstants.Android;
3534
}
3635

37-
this.$androidBundleValidatorHelper.validateNoAab();
3836
this.$projectData.initializeProjectData();
3937
const platforms = this.platform ? [this.platform] : [this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.iOS];
4038

lib/common/child-process.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ export class ChildProcess extends EventEmitter implements IChildProcess {
157157

158158
public async trySpawnFromCloseEvent(command: string, args: string[], options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> {
159159
try {
160-
const childProcess = await this.spawnFromEvent(command, args, "close", options, spawnFromEventOptions);
161-
return childProcess;
160+
const childProcessResult = await this.spawnFromEvent(command, args, "close", options, spawnFromEventOptions);
161+
return childProcessResult;
162162
} catch (err) {
163163
this.$logger.trace(`Error from trySpawnFromCloseEvent method. More info: ${err}`);
164164
return Promise.resolve({ stderr: err && err.message ? err.message : err, stdout: null, exitCode: -1 });

lib/common/declarations.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,11 @@ interface ISysInfo {
10011001
* @return {Promise<string>} Returns the currently installed version of Xcode or null if Xcode is not installed or executed on Linux or Windows.
10021002
*/
10031003
getXcodeVersion(): Promise<string>;
1004+
/**
1005+
* Returns the currently installed Java path based on JAVA_HOME and PATH..
1006+
* @return {Promise<string>} The currently installed Java path.
1007+
*/
1008+
getJavaPath(): Promise<string>;
10041009
/**
10051010
* Returns the currently installed Cocoapods version.
10061011
* @return {Promise<string>} Returns the currently installed Cocoapods version. It will return null if Cocoapods is not installed.

lib/common/definitions/mobile.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,9 @@ declare global {
321321
interface IDeviceApplicationManager extends NodeJS.EventEmitter {
322322
getInstalledApplications(): Promise<string[]>;
323323
isApplicationInstalled(appIdentifier: string): Promise<boolean>;
324-
installApplication(packageFilePath: string, appIdentifier?: string): Promise<void>;
324+
installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IBuildData): Promise<void>;
325325
uninstallApplication(appIdentifier: string): Promise<void>;
326-
reinstallApplication(appIdentifier: string, packageFilePath: string): Promise<void>;
326+
reinstallApplication(appIdentifier: string, packageFilePath: string, buildData?: IBuildData): Promise<void>;
327327
startApplication(appData: IStartApplicationData): Promise<void>;
328328
stopApplication(appData: IApplicationData): Promise<void>;
329329
restartApplication(appData: IStartApplicationData): Promise<void>;

lib/common/mobile/android/android-application-manager.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { ApplicationManagerBase } from "../application-manager-base";
33
import { TARGET_FRAMEWORK_IDENTIFIERS, LiveSyncPaths } from "../../constants";
44
import { hook, sleep, regExpEscape } from "../../helpers";
55
import { cache } from "../../decorators";
6+
import { parse, join } from "path";
7+
import { AAB_EXTENSION_NAME, APKS_EXTENSION_NAME } from "../../../constants";
68

79
export class AndroidApplicationManager extends ApplicationManagerBase {
810
public PID_CHECK_INTERVAL = 100;
911
public PID_CHECK_TIMEOUT = 10000; // 10 secs
1012

1113
constructor(private adb: Mobile.IDeviceAndroidDebugBridge,
1214
private identifier: string,
15+
private $androidBundleToolService: IAndroidBundleToolService,
16+
private $fs: IFileSystem,
1317
private $options: IOptions,
1418
private $logcatHelper: Mobile.ILogcatHelper,
1519
private $androidProcessService: Mobile.IAndroidProcessService,
@@ -33,13 +37,29 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
3337
}
3438

3539
@hook('install')
36-
public async installApplication(packageFilePath: string, appIdentifier?: string): Promise<void> {
40+
public async installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IAndroidBuildData): Promise<void> {
3741
if (appIdentifier) {
3842
const deviceRootPath = `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${appIdentifier}`;
3943
await this.adb.executeShellCommand(["rm", "-rf", deviceRootPath]);
4044
}
4145

42-
return this.adb.executeCommand(["install", "-r", `${packageFilePath}`]);
46+
const { dir, name, ext } = parse(packageFilePath);
47+
if (ext === AAB_EXTENSION_NAME) {
48+
const apksOutputPath = join(dir, name) + APKS_EXTENSION_NAME;
49+
if (!this.hasValidApksFile(packageFilePath, apksOutputPath)) {
50+
await this.$androidBundleToolService.buildApks({
51+
aabFilePath: packageFilePath,
52+
apksOutputPath,
53+
signingData: <IAndroidSigningData>buildData
54+
});
55+
}
56+
await this.$androidBundleToolService.installApks({
57+
apksFilePath: apksOutputPath,
58+
deviceId: this.identifier
59+
});
60+
} else {
61+
return this.adb.executeCommand(["install", "-r", `${packageFilePath}`]);
62+
}
4363
}
4464

4565
public uninstallApplication(appIdentifier: string): Promise<void> {
@@ -155,4 +175,15 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
155175

156176
return new RegExp(`${regExpEscape(appIdentifier)}${packageActivitySeparator}${fullJavaClassName}`, `m`);
157177
}
178+
179+
private hasValidApksFile(aabFilaPath: string, apksFilePath: string): boolean {
180+
let isValid = false;
181+
if (this.$fs.exists(apksFilePath)) {
182+
const lastUpdatedApks = this.$fs.getFsStats(apksFilePath).ctime.getTime();
183+
const lastUpdatedAab = this.$fs.getFsStats(aabFilaPath).ctime.getTime();
184+
isValid = lastUpdatedApks >= lastUpdatedAab;
185+
}
186+
187+
return isValid;
188+
}
158189
}

lib/common/mobile/application-manager-base.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob
1818
}
1919
}
2020

21-
public async reinstallApplication(appIdentifier: string, packageFilePath: string): Promise<void> {
21+
public async reinstallApplication(appIdentifier: string, packageFilePath: string, buildData?: IBuildData): Promise<void> {
2222
const isApplicationInstalled = await this.isApplicationInstalled(appIdentifier);
2323

2424
if (isApplicationInstalled) {
2525
await this.uninstallApplication(appIdentifier);
2626
}
2727

28-
await this.installApplication(packageFilePath, appIdentifier);
28+
await this.installApplication(packageFilePath, appIdentifier, buildData);
2929
}
3030

3131
public async restartApplication(appData: Mobile.IApplicationData): Promise<void> {
@@ -83,7 +83,7 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob
8383
}
8484
}
8585

86-
public abstract async installApplication(packageFilePath: string, appIdentifier?: string): Promise<void>;
86+
public abstract async installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IBuildData): Promise<void>;
8787
public abstract async uninstallApplication(appIdentifier: string): Promise<void>;
8888
public abstract async startApplication(appData: Mobile.IApplicationData): Promise<void>;
8989
public abstract async stopApplication(appData: Mobile.IApplicationData): Promise<void>;

lib/common/test/unit-tests/android-application-manager.ts

+63-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { AndroidApplicationManager } from "../../mobile/android/android-application-manager";
22
import { Yok } from "../../yok";
33
import { assert } from "chai";
4-
import { CommonLoggerStub, LogcatHelperStub, AndroidProcessServiceStub, DeviceLogProviderStub, ErrorsStub } from "./stubs";
4+
import { AndroidBundleToolServiceStub, CommonLoggerStub, HooksServiceStub, LogcatHelperStub, AndroidProcessServiceStub, DeviceLogProviderStub, ErrorsStub } from "./stubs";
5+
import { FileSystemStub } from "../../../../test/stubs";
56
const invalidIdentifier = "invalid.identifier";
67
const validDeviceIdentifier = "device.identifier";
78
const validIdentifier = "org.nativescript.testApp";
89
const validStartOptions = { appId: validIdentifier, projectName: "", projectDir: "" };
10+
const validSigning: any = { keyStoreAlias: "string", keyStorePath: "string", keyStoreAliasPassword: "string", keyStorePassword: "string" };
911

1012
class AndroidDebugBridgeStub {
13+
public calledInstallApplication = false;
1114
public calledStopApplication = false;
1215
public startedWithActivityManager = false;
1316
public validIdentifierPassed = false;
@@ -42,6 +45,12 @@ class AndroidDebugBridgeStub {
4245
frontOfTask=true task=TaskRecord{fe592ac #449 A=org.nativescript.testApp U=0 StackId=1 sz=1}"
4346
];
4447

48+
public async executeCommand(args: string[], options?: any): Promise<any> {
49+
if (args[0] === "install") {
50+
this.calledInstallApplication = true;
51+
}
52+
}
53+
4554
public async executeShellCommand(args: string[]): Promise<any> {
4655
if (args && args.length > 0) {
4756
if (args[0].startsWith("cat")) {
@@ -111,9 +120,11 @@ function createTestInjector(options?: {
111120
testInjector.register("identifier", validDeviceIdentifier);
112121
testInjector.register("logcatHelper", LogcatHelperStub);
113122
testInjector.register("androidProcessService", AndroidProcessServiceStub);
123+
testInjector.register("androidBundleToolService", AndroidBundleToolServiceStub);
124+
testInjector.register("fs", FileSystemStub);
114125
testInjector.register("httpClient", {});
115126
testInjector.register("deviceLogProvider", DeviceLogProviderStub);
116-
testInjector.register("hooksService", {});
127+
testInjector.register("hooksService", HooksServiceStub);
117128
return testInjector;
118129
}
119130

@@ -140,6 +151,7 @@ describe("android-application-manager", () => {
140151
}
141152

142153
describe("startApplication", () => {
154+
143155
it("fires up the right application", async () => {
144156
setup();
145157
for (let i = 0; i < androidDebugBridge.getInputLength(); i++) {
@@ -244,6 +256,55 @@ describe("android-application-manager", () => {
244256
});
245257
});
246258

259+
describe("installApplication", () => {
260+
afterEach(function () {
261+
androidDebugBridge.calledInstallApplication = false;
262+
const bundleToolService = testInjector.resolve<AndroidBundleToolServiceStub>("androidBundleToolService");
263+
bundleToolService.isBuildApksCalled = false;
264+
bundleToolService.isInstallApksCalled = false;
265+
});
266+
267+
it("should install apk using adb", async () => {
268+
const bundleToolService = testInjector.resolve<AndroidBundleToolServiceStub>("androidBundleToolService");
269+
270+
await androidApplicationManager.installApplication("myApp.apk");
271+
272+
assert.isFalse(bundleToolService.isBuildApksCalled);
273+
assert.isFalse(bundleToolService.isInstallApksCalled);
274+
assert.isTrue(androidDebugBridge.calledInstallApplication);
275+
});
276+
277+
it("should install aab using bundletool", async () => {
278+
const bundleToolService = testInjector.resolve<AndroidBundleToolServiceStub>("androidBundleToolService");
279+
280+
await androidApplicationManager.installApplication("myApp.aab");
281+
282+
assert.isTrue(bundleToolService.isBuildApksCalled);
283+
assert.isTrue(bundleToolService.isInstallApksCalled);
284+
assert.isFalse(androidDebugBridge.calledInstallApplication);
285+
});
286+
287+
it("should skip aab build when already built", async () => {
288+
const fsStub = testInjector.resolve<FileSystemStub>("fs");
289+
const bundleToolService = testInjector.resolve<AndroidBundleToolServiceStub>("androidBundleToolService");
290+
291+
await androidApplicationManager.installApplication("myApp.aab", "my.app", validSigning);
292+
293+
assert.isTrue(bundleToolService.isBuildApksCalled);
294+
assert.isTrue(bundleToolService.isInstallApksCalled);
295+
assert.isFalse(androidDebugBridge.calledInstallApplication);
296+
297+
fsStub.fsStatCache["myApp.aab"] = fsStub.fsStatCache["myApp.apks"];
298+
bundleToolService.isBuildApksCalled = false;
299+
bundleToolService.isInstallApksCalled = false;
300+
await androidApplicationManager.installApplication("myApp.aab", "my.app", validSigning);
301+
302+
assert.isFalse(bundleToolService.isBuildApksCalled);
303+
assert.isTrue(bundleToolService.isInstallApksCalled);
304+
assert.isFalse(androidDebugBridge.calledInstallApplication);
305+
});
306+
});
307+
247308
describe("stopApplication", () => {
248309
it("should stop the logcat helper", async () => {
249310
setup();

lib/common/test/unit-tests/stubs.ts

+14
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,17 @@ export class DeviceLogProviderStub extends EventEmitter implements Mobile.IDevic
193193
this.currentDeviceProjectDirs[deviceIdentifier] = projectDir;
194194
}
195195
}
196+
197+
export class AndroidBundleToolServiceStub implements IAndroidBundleToolService {
198+
public isInstallApksCalled = false;
199+
public isBuildApksCalled = false;
200+
201+
buildApks(options: IBuildApksOptions): Promise<void> {
202+
this.isBuildApksCalled = true;
203+
return;
204+
}
205+
installApks(options: IInstallApksOptions): Promise<void> {
206+
this.isInstallApksCalled = true;
207+
return;
208+
}
209+
}

lib/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const CONFIG_NS_APP_ENTRY = "appPath";
5353
export const DEPENDENCIES_JSON_NAME = "dependencies.json";
5454
export const APK_EXTENSION_NAME = ".apk";
5555
export const AAB_EXTENSION_NAME = ".aab";
56+
export const APKS_EXTENSION_NAME = ".apks";
5657
export const HASHES_FILE_NAME = ".nshashes";
5758
export const TNS_NATIVE_SOURCE_GROUP_NAME = "TNSNativeSource";
5859
export const NATIVE_SOURCE_FOLDER = "src";
@@ -288,6 +289,7 @@ export class BundleValidatorMessages {
288289
export class AndroidBundleValidatorMessages {
289290
public static AAB_NOT_SUPPORTED_BY_COMMNAND_MESSAGE = "This command does not support --aab (Android App Bundle) parameter.";
290291
public static NOT_SUPPORTED_RUNTIME_VERSION = "Android App Bundle (--aab) option requires NativeScript Android Runtime (tns-android) version %s and above.";
292+
public static NOT_SUPPORTED_ANDROID_VERSION = "Cannot use the Android App Bundle (--aab) option on device '%s' with Android '%s'. The --aab options is supported on Android '%s' and above.";
291293
}
292294

293295
export class AndroidAppBundleMessages {

lib/declarations.d.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -581,10 +581,10 @@ interface IEnvOptions {
581581
env: Object;
582582
}
583583

584-
interface IAndroidBuildOptionsSettings extends IAndroidReleaseOptions, IRelease, IHasAndroidBundle { }
584+
interface IAndroidBuildOptionsSettings extends IAndroidReleaseOptions, IRelease, Partial<IHasAndroidBundle> { }
585585

586586
interface IHasAndroidBundle {
587-
androidBundle?: boolean;
587+
androidBundle: boolean;
588588
}
589589

590590
interface IPlatformBuildData extends IRelease, IHasUseHotModuleReloadOption, IBuildConfig, IEnvOptions { }
@@ -939,6 +939,14 @@ interface IAndroidBundleValidatorHelper {
939939
* @return {void}
940940
*/
941941
validateRuntimeVersion(projectData: IProjectData): void
942+
943+
/**
944+
* Validates that the specified device supports aab.
945+
* @param {Mobile.IDevice} device The device to be validated.
946+
* @param {IBuildData} buildData The current build data.
947+
* @return {void}
948+
*/
949+
validateDeviceApiLevel(device: Mobile.IDevice, buildData: IBuildData): void
942950
}
943951

944952
interface IOptionsTracker {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface IAndroidBundleToolService {
2+
buildApks(options: IBuildApksOptions): Promise<void>;
3+
installApks(options: IInstallApksOptions): Promise<void>;
4+
}
5+
6+
interface IBuildApksOptions {
7+
aabFilePath: string;
8+
apksOutputPath: string;
9+
signingData: IAndroidSigningData;
10+
}
11+
12+
interface IInstallApksOptions {
13+
apksFilePath: string;
14+
deviceId: string;
15+
}

lib/definitions/build.d.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ interface IiOSBuildData extends IBuildData {
1717
iCloudContainerEnvironment: string;
1818
}
1919

20-
interface IAndroidBuildData extends IBuildData {
20+
interface IAndroidBuildData extends IBuildData, IAndroidSigningData, IHasAndroidBundle {
21+
}
22+
23+
interface IAndroidSigningData {
2124
keyStoreAlias: string;
2225
keyStorePath: string;
2326
keyStoreAliasPassword: string;
2427
keyStorePassword: string;
25-
androidBundle: boolean;
2628
}
2729

2830
interface IBuildController {

lib/definitions/platform.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface IValidBuildOutputData {
3737
regexes?: RegExp[];
3838
}
3939

40-
interface IBuildOutputOptions extends Partial<IBuildForDevice>, IRelease, IHasAndroidBundle {
40+
interface IBuildOutputOptions extends Partial<IBuildForDevice>, IRelease, Partial<IHasAndroidBundle> {
4141
outputPath?: string;
4242
}
4343

0 commit comments

Comments
 (0)