1import path from 'path';
2
3import { resolveInstallApkNameAsync } from './resolveInstallApkName';
4import { Options, ResolvedOptions, resolveOptionsAsync } from './resolveOptions';
5import { Log } from '../../log';
6import { assembleAsync, installAsync } from '../../start/platforms/android/gradle';
7import { setNodeEnv } from '../../utils/nodeEnv';
8import { ensurePortAvailabilityAsync } from '../../utils/port';
9import { getSchemesForAndroidAsync } from '../../utils/scheme';
10import { ensureNativeProjectAsync } from '../ensureNativeProject';
11import { logProjectLogsLocation } from '../hints';
12import { startBundlerAsync } from '../startBundler';
13
14const debug = require('debug')('expo:run:android');
15
16export async function runAndroidAsync(projectRoot: string, { install, ...options }: Options) {
17  // NOTE: This is a guess, the developer can overwrite with `NODE_ENV`.
18  setNodeEnv(options.variant === 'release' ? 'production' : 'development');
19  require('@expo/env').load(projectRoot);
20
21  await ensureNativeProjectAsync(projectRoot, { platform: 'android', install });
22
23  const props = await resolveOptionsAsync(projectRoot, options);
24
25  debug('Package name: ' + props.packageName);
26  Log.log('› Building app...');
27
28  const androidProjectRoot = path.join(projectRoot, 'android');
29
30  await assembleAsync(androidProjectRoot, {
31    variant: props.variant,
32    port: props.port,
33    appName: props.appName,
34    buildCache: props.buildCache,
35  });
36
37  // Ensure the port hasn't become busy during the build.
38  if (props.shouldStartBundler && !(await ensurePortAvailabilityAsync(projectRoot, props))) {
39    props.shouldStartBundler = false;
40  }
41
42  const manager = await startBundlerAsync(projectRoot, {
43    port: props.port,
44    // If a scheme is specified then use that instead of the package name.
45    scheme: (await getSchemesForAndroidAsync(projectRoot))?.[0],
46    headless: !props.shouldStartBundler,
47  });
48
49  await installAppAsync(androidProjectRoot, props);
50
51  await manager.getDefaultDevServer().openCustomRuntimeAsync(
52    'emulator',
53    {
54      applicationId: props.packageName,
55    },
56    { device: props.device.device }
57  );
58
59  if (props.shouldStartBundler) {
60    logProjectLogsLocation();
61  }
62}
63
64async function installAppAsync(androidProjectRoot: string, props: ResolvedOptions) {
65  // Find the APK file path
66  const apkFile = await resolveInstallApkNameAsync(props.device.device, props);
67
68  if (apkFile) {
69    // Attempt to install the APK from the file path
70    const binaryPath = path.join(props.apkVariantDirectory, apkFile);
71    debug('Installing:', binaryPath);
72    await props.device.installAppAsync(binaryPath);
73  } else {
74    // If we cannot resolve the APK file path then we can attempt to install using Gradle.
75    // This offers more advanced resolution that we may not have first class support for.
76    Log.log('› Failed to locate binary file, installing with Gradle...');
77    await installAsync(androidProjectRoot, {
78      variant: props.variant ?? 'debug',
79      appName: props.appName ?? 'app',
80      port: props.port,
81    });
82  }
83}
84