UNPKG

@expo/xdl

Version:
994 lines (784 loc) 57.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.copyInitialShellAppFilesAsync = copyInitialShellAppFilesAsync; exports.runShellAppModificationsAsync = runShellAppModificationsAsync; exports.addDetachedConfigToExp = addDetachedConfigToExp; function _fsExtra() { const data = _interopRequireDefault(require("fs-extra")); _fsExtra = function () { return data; }; return data; } function _glob() { const data = require("glob"); _glob = function () { return data; }; return data; } function _path() { const data = _interopRequireDefault(require("path")); _path = function () { return data; }; return data; } function _replaceString() { const data = _interopRequireDefault(require("replace-string")); _replaceString = function () { return data; }; return data; } function _uuid() { const data = _interopRequireDefault(require("uuid")); _uuid = function () { return data; }; return data; } function _AndroidIcons() { const data = require("./AndroidIcons"); _AndroidIcons = function () { return data; }; return data; } function _AndroidIntentFilters() { const data = _interopRequireDefault(require("./AndroidIntentFilters")); _AndroidIntentFilters = function () { return data; }; return data; } function AssetBundle() { const data = _interopRequireWildcard(require("./AssetBundle")); AssetBundle = function () { return data; }; return data; } function ExponentTools() { const data = _interopRequireWildcard(require("./ExponentTools")); ExponentTools = function () { return data; }; return data; } function _Logger() { const data = _interopRequireDefault(require("./Logger")); _Logger = function () { return data; }; return data; } function _StandaloneBuildFlags() { const data = _interopRequireDefault(require("./StandaloneBuildFlags")); _StandaloneBuildFlags = function () { return data; }; return data; } function _StandaloneContext() { const data = _interopRequireDefault(require("./StandaloneContext")); _StandaloneContext = function () { return data; }; return data; } function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { getManifestAsync, saveUrlToPathAsync, spawnAsyncThrowError, spawnAsync, regexFileAsync, deleteLinesInFileAsync, parseSdkMajorVersion } = ExponentTools(); const imageKeys = ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi']; // Do not call this from anything used by detach function exponentDirectory(workingDir) { if (workingDir) { return workingDir; } else if (process.env.EXPO_UNIVERSE_DIR) { return _path().default.join(process.env.EXPO_UNIVERSE_DIR, 'exponent'); } else { return null; } } function xmlWeirdAndroidEscape(original) { const noAmps = (0, _replaceString().default)(original, '&', '&amp;'); const noLt = (0, _replaceString().default)(noAmps, '<', '&lt;'); const noGt = (0, _replaceString().default)(noLt, '>', '&gt;'); const noApos = (0, _replaceString().default)(noGt, '"', '\\"'); return (0, _replaceString().default)(noApos, "'", "\\'"); } exports.updateAndroidShellAppAsync = async function updateAndroidShellAppAsync(args) { let { url, sdkVersion, releaseChannel, workingDir } = args; releaseChannel = releaseChannel ? releaseChannel : 'default'; const manifest = await getManifestAsync(url, { 'Exponent-SDK-Version': sdkVersion, 'Exponent-Platform': 'android', 'Expo-Release-Channel': releaseChannel, Accept: 'application/expo+json,application/json' }); const fullManifestUrl = url.replace('exp://', 'https://'); const bundleUrl = manifest.bundleUrl; const shellPath = _path().default.join(exponentDirectory(workingDir), 'android-shell-app'); await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getManifestFileNameForSdkVersion(sdkVersion))); await _fsExtra().default.writeFileSync(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getManifestFileNameForSdkVersion(sdkVersion)), JSON.stringify(manifest)); await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getBundleFileNameForSdkVersion(sdkVersion))); await saveUrlToPathAsync(bundleUrl, _path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getBundleFileNameForSdkVersion(sdkVersion))); await deleteLinesInFileAsync(`START EMBEDDED RESPONSES`, `END EMBEDDED RESPONSES`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); await regexFileAsync('// ADD EMBEDDED RESPONSES HERE', ` // ADD EMBEDDED RESPONSES HERE // START EMBEDDED RESPONSES embeddedResponses.add(new Constants.EmbeddedResponse("${fullManifestUrl}", "assets://${ExponentTools().getManifestFileNameForSdkVersion(sdkVersion)}", "application/json")); embeddedResponses.add(new Constants.EmbeddedResponse("${bundleUrl}", "assets://${ExponentTools().getBundleFileNameForSdkVersion(sdkVersion)}", "application/javascript")); // END EMBEDDED RESPONSES`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); await regexFileAsync('RELEASE_CHANNEL = "default"', `RELEASE_CHANNEL = "${releaseChannel}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); }; function backgroundImagesForApp(shellPath, manifest, isDetached, majorSdkVersion) { // returns an array like: // [ // {url: 'urlToDownload', path: 'pathToSaveTo'}, // {url: 'anotherURlToDownload', path: 'anotherPathToSaveTo'}, // ] const splashImageFilename = majorSdkVersion >= 39 ? 'splashscreen_image.png' : 'shell_launch_background_image.png'; const basePath = _path().default.join(shellPath, 'app', 'src', 'main', 'res'); const splash = manifest && manifest.android && manifest.android.splash; if (splash) { const results = imageKeys.reduce(function (acc, imageKey) { const url = isDetached ? splash[imageKey] : splash[`${imageKey}Url`]; if (url) { acc.push({ url, path: _path().default.join(basePath, `drawable-${imageKey}`, splashImageFilename) }); } return acc; }, []); // No splash screen images declared in 'android.splash' configuration, proceed to general one if (results.length !== 0) { return results; } } const url = isDetached ? manifest.splash && manifest.splash.image : manifest.splash && manifest.splash.imageUrl; if (url) { return [{ url, path: _path().default.join(basePath, 'drawable-xxxhdpi', splashImageFilename) }]; } return []; } function getSplashScreenBackgroundColor(manifest) { let backgroundColor; if (manifest.android && manifest.android.splash && manifest.android.splash.backgroundColor) { backgroundColor = manifest.android.splash.backgroundColor; } else if (manifest.splash && manifest.splash.backgroundColor) { backgroundColor = manifest.splash.backgroundColor; } // Default to white if (!backgroundColor) { backgroundColor = '#FFFFFF'; } return backgroundColor; } /* if resizeMode is 'contain' or 'cover' (since SDK33) or 'cover' (prior to SDK33) we should show LoadingView that is presenting splash image in ImageView what allows full control over image sizing unlike ImageDrawable that is provided by Android native splash screen API */ function shouldShowLoadingView(manifest, sdkVersion) { const resizeMode = getSplashImageResizeMode(manifest); return resizeMode && (parseSdkMajorVersion(sdkVersion) >= 33 ? resizeMode === 'contain' || resizeMode === 'cover' : resizeMode === 'cover'); } /** * @return {string | undefined} */ function getSplashImageResizeMode(manifest) { return manifest.android && manifest.android.splash && manifest.android.splash.resizeMode || manifest.splash && manifest.splash.resizeMode; } async function copyInitialShellAppFilesAsync(androidSrcPath, shellPath, isDetached, sdkVersion) { const copyToShellApp = async fileName => { try { await _fsExtra().default.copy(_path().default.join(androidSrcPath, fileName), _path().default.join(shellPath, fileName)); } catch (e) { // android.iml is only available locally, not on the builders, so don't crash when this happens if (e.code === 'ENOENT') {// Some files are not included in all ExpoKit versions, so this error can be ignored. } else { throw new Error(`Could not copy ${fileName} to shell app directory: ${e.message}`); } } }; if (!isDetached) { await copyToShellApp('expoview'); await copyToShellApp('versioned-abis'); await copyToShellApp('ReactCommon'); await copyToShellApp('ReactAndroid'); } await copyToShellApp('android.iml'); await copyToShellApp('app'); await copyToShellApp('build.gradle'); await copyToShellApp('gradle'); await copyToShellApp('gradle.properties'); await copyToShellApp('gradlew'); await copyToShellApp('settings.gradle'); await copyToShellApp('versioning_linking.gradle'); await copyToShellApp('debug.keystore'); await copyToShellApp('run.sh'); await copyToShellApp('maven'); // this is a symlink // kernel.android.bundle isn't ever used in standalone apps (at least in kernel v32) // but in order to not change behavior in older SDKs, we'll remove the file only in 32+. if (parseSdkMajorVersion(sdkVersion) >= 32) { try { await _fsExtra().default.remove(_path().default.join(shellPath, 'app/src/main/assets/kernel.android.bundle')); } catch (e) {// let's hope it's just not present in the shell app template } } } exports.createAndroidShellAppAsync = async function createAndroidShellAppAsync(args) { let { url, sdkVersion, releaseChannel, privateConfigFile, configuration, keystore, alias, keystorePassword, keyPassword, outputFile, workingDir, modules, buildType, buildMode, gradleArgs } = args; const exponentDir = exponentDirectory(workingDir); const androidSrcPath = _path().default.join(exponentDir, 'android'); const shellPath = _path().default.join(exponentDir, 'android-shell-app'); await _fsExtra().default.remove(shellPath); await _fsExtra().default.ensureDir(shellPath); releaseChannel = releaseChannel ? releaseChannel : 'default'; let manifest; if (args.manifest) { manifest = args.manifest; _Logger().default.withFields({ buildPhase: 'reading manifest' }).info('Using manifest:', JSON.stringify(manifest)); } else { manifest = await getManifestAsync(url, { 'Exponent-SDK-Version': sdkVersion, 'Exponent-Platform': 'android', 'Expo-Release-Channel': releaseChannel, Accept: 'application/expo+json,application/json' }); } configuration = configuration ? configuration : 'Release'; let privateConfig; if (privateConfigFile) { const privateConfigContents = await _fsExtra().default.readFile(privateConfigFile, 'utf8'); privateConfig = JSON.parse(privateConfigContents); } else if (manifest.android) { privateConfig = manifest.android.config; } let androidBuildConfiguration; if (keystore && alias && keystorePassword && keyPassword) { androidBuildConfiguration = { keystore, keystorePassword, keyAlias: alias, keyPassword, outputFile }; } const buildFlags = _StandaloneBuildFlags().default.createAndroid(configuration, androidBuildConfiguration); const context = _StandaloneContext().default.createServiceContext(androidSrcPath, null, manifest, privateConfig, /* testEnvironment */ 'none', buildFlags, url, releaseChannel, null); await copyInitialShellAppFilesAsync(androidSrcPath, shellPath, false, sdkVersion); await removeObsoleteSdks(shellPath, sdkVersion); await runShellAppModificationsAsync(context, sdkVersion, buildMode); await prepareEnabledModules(shellPath, modules); if (!args.skipBuild) { await buildShellAppAsync(context, sdkVersion, buildType, buildMode, gradleArgs); } }; function shellPathForContext(context) { if (context.type === 'user') { return _path().default.join(context.data.projectPath, 'android'); } else { return _path().default.join(exponentDirectory(context.data.expoSourcePath && _path().default.join(context.data.expoSourcePath, '..')), 'android-shell-app'); } } /** * Resolve the private config for a project. * For standalone apps, this is copied into a separate context field context.data.privateConfig * by the turtle builder. For a local project, this is available in app.json under android.config. */ function getPrivateConfig(context) { if (context.data.privateConfig) { return context.data.privateConfig; } else { const exp = context.data.exp; if (exp && exp.android) { return exp.android.config; } } } function ensureStringArray(input) { // Normalize schemes and filter invalid schemes. const stringArray = (Array.isArray(input) ? input : [input]).filter(scheme => typeof scheme === 'string' && !!scheme); return stringArray; } async function runShellAppModificationsAsync(context, sdkVersion, buildMode) { var _manifest$detach, _manifest$android, _manifest$notificatio, _manifest$notificatio2; const fnLogger = _Logger().default.withFields({ buildPhase: 'running shell app modifications' }); const shellPath = shellPathForContext(context); const url = context.published.url; const manifest = context.config; // manifest or app.json const releaseChannel = context.published.releaseChannel; const isRunningInUserContext = context.type === 'user'; // In SDK32 we've unified build process for shell and ejected apps const isDetached = ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32 || isRunningInUserContext; const privateConfig = getPrivateConfig(context); if (!privateConfig) { fnLogger.info('No config file specified.'); } const fullManifestUrl = url.replace('exp://', 'https://'); let versionCode = 1; const javaPackage = manifest.android.package; if (manifest.android.versionCode) { versionCode = manifest.android.versionCode; } if (!javaPackage) { throw new Error('Must specify androidPackage option (either from manifest or on command line).'); } const name = manifest.name; const multiPlatformSchemes = ensureStringArray(manifest.scheme || ((_manifest$detach = manifest.detach) === null || _manifest$detach === void 0 ? void 0 : _manifest$detach.scheme)); const androidSchemes = ensureStringArray((_manifest$android = manifest.android) === null || _manifest$android === void 0 ? void 0 : _manifest$android.scheme); const schemes = [...multiPlatformSchemes, ...androidSchemes]; const scheme = schemes[0]; const bundleUrl = manifest.bundleUrl; const isFullManifest = !!bundleUrl; const version = manifest.version ? manifest.version : '0.0.0'; const majorSdkVersion = parseSdkMajorVersion(sdkVersion); const backgroundImages = backgroundImagesForApp(shellPath, manifest, isRunningInUserContext, majorSdkVersion); const splashBackgroundColor = getSplashScreenBackgroundColor(manifest); const updatesDisabled = manifest.updates && manifest.updates.enabled === false; const updatesCheckAutomaticallyDisabled = manifest.updates && manifest.updates.checkAutomatically === 'ON_ERROR_RECOVERY'; const fallbackToCacheTimeout = manifest.updates && manifest.updates.fallbackToCacheTimeout; // Clean build directories await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'build')); await _fsExtra().default.remove(_path().default.join(shellPath, 'ReactAndroid', 'build')); await _fsExtra().default.remove(_path().default.join(shellPath, 'expoview', 'build')); await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'test')); await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'androidTest')); if (isDetached) { const rootBuildGradle = _path().default.join(shellPath, 'build.gradle'); const appBuildGradle = _path().default.join(shellPath, 'app', 'build.gradle'); if (isRunningInUserContext) { await regexFileAsync(/\/\* UNCOMMENT WHEN DETACHING/g, '', appBuildGradle); await regexFileAsync(/END UNCOMMENT WHEN DETACHING \*\//g, '', appBuildGradle); await deleteLinesInFileAsync('WHEN_DETACHING_REMOVE_FROM_HERE', 'WHEN_DETACHING_REMOVE_TO_HERE', appBuildGradle); } await regexFileAsync(/\/\* UNCOMMENT WHEN DISTRIBUTING/g, '', appBuildGradle); await regexFileAsync(/END UNCOMMENT WHEN DISTRIBUTING \*\//g, '', appBuildGradle); await deleteLinesInFileAsync('WHEN_DISTRIBUTING_REMOVE_FROM_HERE', 'WHEN_DISTRIBUTING_REMOVE_TO_HERE', appBuildGradle); await deleteLinesInFileAsync('WHEN_DISTRIBUTING_REMOVE_FROM_HERE', 'WHEN_DISTRIBUTING_REMOVE_TO_HERE', rootBuildGradle); if (majorSdkVersion >= 33) { const settingsGradle = _path().default.join(shellPath, 'settings.gradle'); await regexFileAsync(/\/\* UNCOMMENT WHEN DISTRIBUTING/g, '', settingsGradle); await regexFileAsync(/END UNCOMMENT WHEN DISTRIBUTING \*\//g, '', settingsGradle); await deleteLinesInFileAsync('WHEN_DISTRIBUTING_REMOVE_FROM_HERE', 'WHEN_DISTRIBUTING_REMOVE_TO_HERE', settingsGradle); } else { // Don't need to compile expoview or ReactAndroid // react-native link looks for a \n so we need that. See https://github.com/facebook/react-native/blob/master/local-cli/link/android/patches/makeSettingsPatch.js await _fsExtra().default.writeFile(_path().default.join(shellPath, 'settings.gradle'), `include ':app'\n`); } await Promise.all(['MainActivity.java', 'MainActivity.kt'].map(file => { const target = _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', file); return _fsExtra().default.existsSync(target) ? regexFileAsync('TEMPLATE_INITIAL_URL', url, target) : Promise.resolve(); })); const runShPath = _path().default.join(shellPath, 'run.sh'); if (await _fsExtra().default.pathExists(runShPath)) { await regexFileAsync('host.exp.exponent/', `${javaPackage}/`, runShPath); await regexFileAsync('LauncherActivity', 'MainActivity', runShPath); } } // Package await regexFileAsync(`applicationId 'host.exp.exponent'`, `applicationId '${javaPackage}'`, _path().default.join(shellPath, 'app', 'build.gradle')); await regexFileAsync(`android:name="host.exp.exponent"`, `android:name="${javaPackage}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Versions await regexFileAsync('VERSION_NAME = null', `VERSION_NAME = "${version}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); await deleteLinesInFileAsync(`BEGIN VERSIONS`, `END VERSIONS`, _path().default.join(shellPath, 'app', 'build.gradle')); await regexFileAsync('// ADD VERSIONS HERE', `versionCode ${versionCode} versionName '${version}'`, _path().default.join(shellPath, 'app', 'build.gradle')); // Remove Exponent build script, since SDK32 expoview comes precompiled if (majorSdkVersion < 32 && !isRunningInUserContext) { await regexFileAsync(`preBuild.dependsOn generateDynamicMacros`, ``, _path().default.join(shellPath, 'expoview', 'build.gradle')); } // change javaMaxHeapSize await regexFileAsync(`javaMaxHeapSize "8g"`, `javaMaxHeapSize "6g"`, _path().default.join(shellPath, 'app', 'build.gradle')); // TODO: probably don't need this in both places await regexFileAsync(/host\.exp\.exponent\.permission\.C2D_MESSAGE/g, `${javaPackage}.permission.C2D_MESSAGE`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Since SDK32 expoview comes precompiled if (majorSdkVersion < 32 && !isRunningInUserContext) { await regexFileAsync(/host\.exp\.exponent\.permission\.C2D_MESSAGE/g, `${javaPackage}.permission.C2D_MESSAGE`, _path().default.join(shellPath, 'expoview', 'src', 'main', 'AndroidManifest.xml')); } // Set INITIAL_URL, SHELL_APP_SCHEME and SHOW_LOADING_VIEW await regexFileAsync('INITIAL_URL = null', `INITIAL_URL = "${url}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); if (scheme) { await regexFileAsync('SHELL_APP_SCHEME = null', `SHELL_APP_SCHEME = "${scheme}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } if (majorSdkVersion < 39) { // Handle 'contain' and 'cover' splashScreen mode by showing only background color and then actual splashScreen image inside AppLoadingView if (shouldShowLoadingView(manifest, sdkVersion)) { await regexFileAsync('SHOW_LOADING_VIEW_IN_SHELL_APP = false', 'SHOW_LOADING_VIEW_IN_SHELL_APP = true', _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); // show only background color if LoadingView will appear await regexFileAsync(/<item>.*<\/item>/, '', _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'drawable', 'splash_background.xml')); } } // Android SplashScreen is showing custom ImageView, but when imageResizeMode is set to 'native' // it also shows the static image from the very start of the app if (majorSdkVersion >= 39 && getSplashImageResizeMode(manifest) !== 'native') { // template is assuming that we're actually showing the 'native' mode splash screen, // so if we're not we need to remove it await regexFileAsync(/<item>(.|\s)*?<\/item>/, '', _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'drawable', 'splashscreen.xml')); } // Pass the correct SplashScreenImageResizeMode into the AppConstants if (majorSdkVersion >= 39) { await regexFileAsync(/SPLASH_SCREEN_IMAGE_RESIZE_MODE = SplashScreenImageResizeMode\..*?;/, `SPLASH_SCREEN_IMAGE_RESIZE_MODE = SplashScreenImageResizeMode.${(getSplashImageResizeMode(manifest) || 'contain').toUpperCase()};`, _path().default.join(shellPath, 'app/src/main/java/host/exp/exponent/generated/AppConstants.java')); } // In SDK32 this field got removed from AppConstants if (majorSdkVersion < 32 && isRunningInUserContext) { await regexFileAsync('IS_DETACHED = false', `IS_DETACHED = true`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } if (updatesDisabled) { await regexFileAsync('ARE_REMOTE_UPDATES_ENABLED = true', 'ARE_REMOTE_UPDATES_ENABLED = false', _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } if (majorSdkVersion >= 39 && updatesCheckAutomaticallyDisabled) { await regexFileAsync('UPDATES_CHECK_AUTOMATICALLY = true', 'UPDATES_CHECK_AUTOMATICALLY = false', _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } if (majorSdkVersion >= 39 && fallbackToCacheTimeout) { await regexFileAsync('UPDATES_FALLBACK_TO_CACHE_TIMEOUT = 0', `UPDATES_FALLBACK_TO_CACHE_TIMEOUT = ${fallbackToCacheTimeout}`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } // App name await regexFileAsync('"app_name">Expo<', `"app_name">${xmlWeirdAndroidEscape(name)}<`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')); await regexFileAsync('"app_name">Expo Go<', `"app_name">${xmlWeirdAndroidEscape(name)}<`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')); await regexFileAsync('"versioned_app_name">Expo Go<', `"versioned_app_name">${xmlWeirdAndroidEscape(name)}<`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')); await regexFileAsync(/^\s*<string name="unversioned_app_name">Expo Go \(unversioned\)<\/string>\s*$/m, '', _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')); // Splash Screen background color if (majorSdkVersion >= 39) { await regexFileAsync('"splashscreen_background">#FFFFFF', `"splashscreen_background">${splashBackgroundColor}`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'colors.xml')); } else { await regexFileAsync('"splashBackground">#FFFFFF', `"splashBackground">${splashBackgroundColor}`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'colors.xml')); } // Change stripe schemes and add meta-data const randomID = _uuid().default.v4(); const newScheme = `<meta-data android:name="standaloneStripeScheme" android:value="${randomID}" />`; await regexFileAsync('<!-- ADD HERE STRIPE SCHEME META DATA -->', newScheme, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); const newSchemeSuffix = `expo.modules.payments.stripe.${randomID}" />`; await regexFileAsync('expo.modules.payments.stripe" />', newSchemeSuffix, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Remove exp:// scheme from LauncherActivity await deleteLinesInFileAsync(`START LAUNCHER INTENT FILTERS`, `END LAUNCHER INTENT FILTERS`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Remove LAUNCHER category from HomeActivity await deleteLinesInFileAsync(`START HOME INTENT FILTERS`, `END HOME INTENT FILTERS`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); if (isDetached) { // Add LAUNCHER category to MainActivity await regexFileAsync('<!-- ADD DETACH INTENT FILTERS HERE -->', `<intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } else { // Add LAUNCHER category to ShellAppActivity await regexFileAsync('<!-- ADD SHELL INTENT FILTERS HERE -->', `<intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Add app-specific intent filters const intentFilters = manifest.android.intentFilters; if (intentFilters) { if (isDetached) { await regexFileAsync('<!-- ADD DETACH APP SPECIFIC INTENT FILTERS -->', (0, _AndroidIntentFilters().default)(intentFilters).join('\n'), _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } else { await regexFileAsync('<!-- ADD SHELL APP SPECIFIC INTENT FILTERS -->', (0, _AndroidIntentFilters().default)(intentFilters).join('\n'), _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } } // Add shell app scheme if (schemes.length > 0) { const searchLine = isDetached ? '<!-- ADD DETACH SCHEME HERE -->' : '<!-- ADD SHELL SCHEME HERE -->'; const schemesTags = schemes.map(scheme => `<data android:scheme="${scheme}"/>`).join(` `); await regexFileAsync(searchLine, `<intent-filter> ${schemesTags} <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> </intent-filter>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Add Facebook app scheme if (manifest.facebookScheme) { await regexFileAsync('<!-- REPLACE WITH FACEBOOK SCHEME -->', `<data android:scheme="${manifest.facebookScheme}" />`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } if (manifest.android && manifest.android.allowBackup === false) { await regexFileAsync(`android:allowBackup="true"`, `android:allowBackup="false"`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Add permissions if (manifest.android && manifest.android.permissions) { const whitelist = []; manifest.android.permissions.forEach(s => { if (s.includes('.')) { whitelist.push(s); } else { // If shorthand form like `WRITE_CONTACTS` is provided, expand it to `android.permission.WRITE_CONTACTS`. whitelist.push(`android.permission.${s}`); } }); // Permissions we need to remove from the generated manifest const blacklist = [// module permissions - included by default / requires user to add to `android.permissions` list 'android.permission.ACCESS_COARSE_LOCATION', // expo-bluetooth, expo-location 'android.permission.ACCESS_FINE_LOCATION', // expo-location 'android.permission.ACCESS_BACKGROUND_LOCATION', // expo-location 'android.permission.CAMERA', // expo-barcode-scanner, expo-camera, expo-image-picker 'android.permission.RECORD_AUDIO', // expo-camera 'android.permission.READ_CONTACTS', // expo-contacts 'android.permission.WRITE_CONTACTS', // expo-contacts 'android.permission.READ_CALENDAR', // expo-calendar 'android.permission.WRITE_CALENDAR', // expo-calendar 'android.permission.READ_EXTERNAL_STORAGE', // expo-file-system, expo-image, expo-media-library, expo-video-thumbnails 'android.permission.WRITE_EXTERNAL_STORAGE', // expo-file-system, expo-media-library 'android.permission.USE_FINGERPRINT', // expo-local-authentication 'android.permission.USE_BIOMETRIC', // expo-local-authentication 'android.permission.VIBRATE', // expo-haptics 'android.permission.ACCESS_MEDIA_LOCATION', // android-media-library // react native debugging permissions - not required for standalone apps 'android.permission.READ_PHONE_STATE', 'android.permission.SYSTEM_ALERT_WINDOW', // signature permissions - not available for 3rd party 'android.permission.MANAGE_DOCUMENTS', // https://github.com/expo/expo/pull/9727 'android.permission.READ_SMS', // https://github.com/expo/expo/pull/2982 'android.permission.REQUEST_INSTALL_PACKAGES', // https://github.com/expo/expo/pull/8969 // other permissions 'android.permission.READ_INTERNAL_STORAGE', 'com.anddoes.launcher.permission.UPDATE_COUNT', 'com.android.launcher.permission.INSTALL_SHORTCUT', 'com.google.android.gms.permission.ACTIVITY_RECOGNITION', 'com.google.android.providers.gsf.permission.READ_GSERVICES', 'com.htc.launcher.permission.READ_SETTINGS', 'com.htc.launcher.permission.UPDATE_SHORTCUT', 'com.majeur.launcher.permission.UPDATE_BADGE', 'com.sec.android.provider.badge.permission.READ', 'com.sec.android.provider.badge.permission.WRITE', 'com.sonyericsson.home.permission.BROADCAST_BADGE'].filter(p => !whitelist.includes(p)); await deleteLinesInFileAsync(`BEGIN OPTIONAL PERMISSIONS`, `END OPTIONAL PERMISSIONS`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); await regexFileAsync('<!-- ADD PERMISSIONS HERE -->', ` ${whitelist.map(p => `<uses-permission android:name="${p}" />`).join('\n')} ${blacklist.map(p => `<uses-permission android:name="${p}" tools:node="remove" />`).join('\n')} `, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // OAuth redirect scheme await regexFileAsync('<data android:scheme="host.exp.exponent" android:path="oauthredirect"/>', `<data android:scheme="${javaPackage}" android:path="oauthredirect"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Embed manifest and bundle if (isFullManifest) { await _fsExtra().default.writeFileSync(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getManifestFileNameForSdkVersion(sdkVersion)), JSON.stringify(manifest)); await saveUrlToPathAsync(bundleUrl, _path().default.join(shellPath, 'app', 'src', 'main', 'assets', ExponentTools().getBundleFileNameForSdkVersion(sdkVersion))); await regexFileAsync('// START EMBEDDED RESPONSES', ` // START EMBEDDED RESPONSES embeddedResponses.add(new Constants.EmbeddedResponse("${fullManifestUrl}", "assets://${ExponentTools().getManifestFileNameForSdkVersion(sdkVersion)}", "application/json")); embeddedResponses.add(new Constants.EmbeddedResponse("${bundleUrl}", "assets://${ExponentTools().getBundleFileNameForSdkVersion(sdkVersion)}", "application/javascript"));`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } await regexFileAsync('RELEASE_CHANNEL = "default"', `RELEASE_CHANNEL = "${releaseChannel}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); // Icons (0, _AndroidIcons().createAndWriteIconsToPathAsync)(context, _path().default.join(shellPath, 'app', 'src', 'main', 'res'), isRunningInUserContext); // Set the tint color for icons in the notification tray // This is set to "#005eff" in the Expo Go app, but // just to be safe we'll match any value with a regex await regexFileAsync(/"notification_icon_color">.*?</, `"notification_icon_color">${(_manifest$notificatio = (_manifest$notificatio2 = manifest.notification) === null || _manifest$notificatio2 === void 0 ? void 0 : _manifest$notificatio2.color) !== null && _manifest$notificatio !== void 0 ? _manifest$notificatio : '#ffffff'}<`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'colors.xml')); // Delete the placeholder splash images const splashImageFilename = majorSdkVersion >= 39 ? 'splashscreen_image.png' : 'shell_launch_background_image.png'; (0, _glob().sync)(`**/${splashImageFilename}`, { cwd: _path().default.join(shellPath, 'app', 'src', 'main', 'res'), absolute: true }).forEach(filePath => { _fsExtra().default.removeSync(filePath); }); // Use the splash images provided by the user if (backgroundImages && backgroundImages.length > 0) { await Promise.all(backgroundImages.map(async image => { if (isRunningInUserContext) { // local file so just copy it await _fsExtra().default.copy(_path().default.resolve(context.data.projectPath, image.url), image.path); } else { await saveUrlToPathAsync(image.url, image.path); } })); } await AssetBundle().bundleAsync(context, manifest.bundledAssets, `${shellPath}/app/src/main/assets`); let certificateHash = ''; let googleAndroidApiKey = ''; if (privateConfig) { const branch = privateConfig.branch; const fabric = privateConfig.fabric; const googleMaps = privateConfig.googleMaps; const googleSignIn = privateConfig.googleSignIn; const googleMobileAdsAppId = privateConfig.googleMobileAdsAppId; const googleMobileAdsAutoInit = privateConfig.googleMobileAdsAutoInit; // Branch if (branch) { await regexFileAsync('<!-- ADD BRANCH CONFIG HERE -->', `<meta-data android:name="io.branch.sdk.BranchKey" android:value="${branch.apiKey}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Fabric // Delete existing Fabric API key. await deleteLinesInFileAsync(`BEGIN FABRIC CONFIG`, `END FABRIC CONFIG`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'fabric.properties')); if (fabric) { // Put user's Fabric key if provided. await _fsExtra().default.writeFileSync(_path().default.join(shellPath, 'app', 'fabric.properties'), `apiSecret=${fabric.buildSecret}\n`); await regexFileAsync('<!-- ADD FABRIC CONFIG HERE -->', `<meta-data android:name="io.fabric.ApiKey" android:value="${fabric.apiKey}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Google Maps // Delete existing Google Maps API key. await deleteLinesInFileAsync(`BEGIN GOOGLE MAPS CONFIG`, `END GOOGLE MAPS CONFIG`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); if (googleMaps) { // Put user's Google Maps API key if provided. await regexFileAsync('<!-- ADD GOOGLE MAPS CONFIG HERE -->', `<meta-data android:name="com.google.android.geo.API_KEY" android:value="${googleMaps.apiKey}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Google Mobile Ads App ID // The app crashes if the app ID isn't provided, so if the user // doesn't provide the ID, we leave the sample one. if (googleMobileAdsAppId) { // Delete existing Google Mobile Ads App ID. await deleteLinesInFileAsync(`BEGIN GOOGLE MOBILE ADS CONFIG`, `END GOOGLE MOBILE ADS CONFIG`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); // Put user's Google Mobile Ads App ID if provided. await regexFileAsync('<!-- ADD GOOGLE MOBILE ADS CONFIG HERE -->', `<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="${googleMobileAdsAppId}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Auto-init of Google App Measurement // unless the user explicitly specifies they want to auto-init, we leave delay set to true if (googleMobileAdsAutoInit) { await regexFileAsync('<meta-data android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT" android:value="true"/>', '<meta-data android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT" android:value="false"/>', _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // Google Login if (googleSignIn) { certificateHash = googleSignIn.certificateHash; googleAndroidApiKey = googleSignIn.apiKey; } } if (manifest.android && manifest.android.googleServicesFile) { // google-services.json // Used for configuring FCM let googleServicesFileContents = manifest.android.googleServicesFile; if (isRunningInUserContext) { googleServicesFileContents = await _fsExtra().default.readFile(_path().default.resolve(shellPath, '..', manifest.android.googleServicesFile), 'utf8'); } await _fsExtra().default.writeFile(_path().default.join(shellPath, 'app', 'google-services.json'), googleServicesFileContents); } else { await regexFileAsync('FCM_ENABLED = true', 'FCM_ENABLED = false', _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java')); } // Configure google-services.json // It is either already in the shell app (placeholder or real one) or it has been added // from `manifest.android.googleServicesFile`. if (parseSdkMajorVersion(sdkVersion) >= 37) { // New behavior - googleServicesFile overrides googleSignIn configuration if (!manifest.android || !manifest.android.googleServicesFile) { // No google-services.json file, let's use the placeholder one // and insert keys manually ("the old way"). // This seems like something we can't really get away without: // // 1. A project with google-services plugin won't build without // a google-services.json file (the plugin makes sure to fail the build). // 2. Adding the google-services plugin conditionally and properly (to be able // to build a project without that file) is too much hassle. // // So, let's use `host.exp.exponent` as a placeholder for the project's // javaPackage. In custom shell apps there shouldn't ever be "host.exp.exponent" // so this shouldn't cause any problems for advanced users. await regexFileAsync('"package_name": "host.exp.exponent"', `"package_name": "${javaPackage}"`, _path().default.join(shellPath, 'app', 'google-services.json')); // Let's not modify values of the original google-services.json file // if they haven't been provided (in which case, googleAndroidApiKey // and certificateHash will be an empty string). This will make sure // we don't modify shell app's google-services needlessly. // Google sign in if (googleAndroidApiKey) { await regexFileAsync(/"current_key": "(.*?)"/, `"current_key": "${googleAndroidApiKey}"`, _path().default.join(shellPath, 'app', 'google-services.json')); } if (certificateHash) { await regexFileAsync(/"certificate_hash": "(.*?)"/, `"certificate_hash": "${certificateHash}"`, _path().default.join(shellPath, 'app', 'google-services.json')); } } else if (googleAndroidApiKey || certificateHash) { // Both googleServicesFile and googleSignIn configuration have been provided. // Let's print a helpful warning and not modify google-services.json. fnLogger.warn('You have provided values for both `googleServicesFile` and `googleSignIn` in your `app.json`. Since SDK37 `googleServicesFile` overrides any other `google-services.json`-related configuration. Recommended way to fix this warning is to remove `googleSignIn` configuration from your `app.json` in favor of using only `googleServicesFile` to configure Google services.'); } } else { // Old behavior - googleSignIn overrides googleServicesFile if (manifest.android && manifest.android.googleServicesFile) { // googleServicesFile provided, let's warn the user that its contents // are about to be modified. fnLogger.warn('You have provided a custom `googleServicesFile` in your `app.json`. In projects below SDK37 `googleServicesFile` contents will be overridden with `googleSignIn` configuration. (Even if there is none, in which case the API key from `google-services.json` will be removed.) To mitigate this behavior upgrade to SDK37 or check out https://github.com/expo/expo/issues/7727#issuecomment-611544439 for a workaround.'); } // Push notifications await regexFileAsync('"package_name": "host.exp.exponent"', `"package_name": "${javaPackage}"`, _path().default.join(shellPath, 'app', 'google-services.json')); // Google sign in await regexFileAsync(/"current_key": "(.*?)"/, `"current_key": "${googleAndroidApiKey}"`, _path().default.join(shellPath, 'app', 'google-services.json')); await regexFileAsync(/"certificate_hash": "(.*?)"/, `"certificate_hash": "${certificateHash}"`, _path().default.join(shellPath, 'app', 'google-services.json')); } // Set manifest url for debug mode if (buildMode === 'debug') { await regexFileAsync('DEVELOPMENT_URL = ""', `DEVELOPMENT_URL = "${fullManifestUrl}"`, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'DetachBuildConstants.java')); } // Facebook configuration // There's no such pattern to replace in shell apps below SDK 36, // so this will not have any effect on these apps. if (manifest.facebookAppId) { await regexFileAsync('<!-- ADD FACEBOOK APP ID CONFIG HERE -->', `<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="${manifest.facebookAppId}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // There's no such pattern to replace in shell apps below SDK 36, // so this will not have any effect on these apps. if (manifest.facebookDisplayName) { await regexFileAsync('<!-- ADD FACEBOOK APP DISPLAY NAME CONFIG HERE -->', `<meta-data android:name="com.facebook.sdk.ApplicationName" android:value="${manifest.facebookDisplayName}"/>`, _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // There's no such pattern to replace in shell apps below SDK 36, // so this will not have any effect on these apps. if (manifest.facebookAutoInitEnabled) { await regexFileAsync('<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/>', '<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="true"/>', _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // There's no such pattern to replace in shell apps below SDK 36, // so this will not have any effect on these apps. if (manifest.facebookAutoLogAppEventsEnabled) { await regexFileAsync('<meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="false"/>', '<meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="true"/>', _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } // There's no such pattern to replace in shell apps below SDK 36, // so this will not have any effect on these apps. if (manifest.facebookAdvertiserIDCollectionEnabled) { await regexFileAsync('<meta-data android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled" android:value="false"/>', '<meta-data android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled" android:value="true"/>', _path().default.join(shellPath, 'app', 'src', 'main', 'AndroidManifest.xml')); } } async function buildShellAppAsync(context, sdkVersion, buildType, buildMode, userProvidedGradleArgs) { const shellPath = shellPathForContext(context); const ext = buildType === 'app-bundle' ? 'aab' : 'apk'; const isRelease = !!context.build.android && buildMode === 'release'; // concat on those strings is not very readable, but only alternative here is huge if statement const debugOrRelease = isRelease ? 'Release' : 'Debug'; const devOrProd = isRelease ? 'Prod' : 'Dev'; const debugOrReleaseL = isRelease ? 'release' : 'debug'; const devOrProdL = isRelease ? 'prod' : 'dev'; const shellFile = `shell.${ext}`; const shellUnalignedFile = `shell-unaligned.${ext}`; const outputDirPath = _path().default.join(shellPath, 'app', 'build', 'outputs', buildType === 'app-bundle' ? 'bundle' : 'apk'); let gradleBuildCommand; let outputPath; if (buildType === 'app-bundle') { if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 36) { gradleBuildCommand = `:app:bundle${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, debugOrReleaseL, `app-${debugOrReleaseL}.aab`); } else if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 33) { gradleBuildCommand = `:app:bundle${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, debugOrReleaseL, `app.aab`); } else if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32) { gradleBuildCommand = `:app:bundle${devOrProd}Kernel${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, `${devOrProdL}Kernel${debugOrRelease}`, `app.aab`); } else { // gradleBuildCommand = `:app:bundle${devOrProd}MinSdk${devOrProd}Kernel${debugOrRelease}`; // outputPath = path.join( // outputDirPath, // `${devOrProdL}MinSdk${devOrProd}Kernel`, // debugOrReleaseL, // `app.aab` // ); // TODO (wkozyra95) debug building app bundles for sdk 31 and older // for now it has low priority throw new Error('Android App Bundles are not supported for sdk31 and lower'); } } else { if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 33) { gradleBuildCommand = `:app:assemble${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, debugOrReleaseL, `app-${debugOrReleaseL}.apk`); } else if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32) { gradleBuildCommand = `:app:assemble${devOrProd}Kernel${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, `${devOrProdL}Kernel`, debugOrReleaseL, `app-${devOrProdL}Kernel-${debugOrReleaseL}.apk`); } else { gradleBuildCommand = `:app:assemble${devOrProd}MinSdk${devOrProd}Kernel${debugOrRelease}`; outputPath = _path().default.join(outputDirPath, `${devOrProdL}MinSdk${devOrProd}Kernel`, debugOrReleaseL, `app-${devOrProdL}MinSdk-${devOrProdL}Kernel-${debugOrReleaseL}-unsigned.apk`); } } await ExponentTools().removeIfExists(shellUnalignedFile); await ExponentTools().removeIfExists(shellFile); await ExponentTools().removeIfExists(outputPath); if (isRelease) { const androidBuildConfiguration = context.build.android; const gradleArgs = [...(userProvidedGradleArgs || []), gradleBuildCommand]; if (process.env.GRADLE_DAEMON_DISABLED) { gradleArgs.unshift('--no-daemon'); } await spawnAsyncThrowError(`./gradlew`, gradleArgs, { pipeToLogger: true, loggerFields: { buildPhase: 'running gradle' }, cwd: shellPath, env: { ...process.env, ANDROID_KEY_ALIAS: androidBuildConfiguration.keyAlias, ANDROID_KEY_PASSWORD: androidBuildConfiguration.keyPassword