"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 _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 _lodash() {
  const data = _interopRequireDefault(require("lodash"));

  _lodash = function () {
    return data;
  };

  return data;
}

function _globby() {
  const data = _interopRequireDefault(require("globby"));

  _globby = 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 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 _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 _AndroidIntentFilters() {
  const data = _interopRequireDefault(require("./AndroidIntentFilters"));

  _AndroidIntentFilters = function () {
    return data;
  };

  return data;
}

function _Logger() {
  const data = _interopRequireDefault(require("./Logger"));

  _Logger = function () {
    return data;
  };

  return data;
}

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; 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) {
  let noAmps = (0, _replaceString().default)(original, '&', '&');
  let noLt = (0, _replaceString().default)(noAmps, '<', '&lt;');
  let noGt = (0, _replaceString().default)(noLt, '>', '&gt;');
  let 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';
  let manifest = await getManifestAsync(url, {
    'Exponent-SDK-Version': sdkVersion,
    'Exponent-Platform': 'android',
    'Expo-Release-Channel': releaseChannel,
    Accept: 'application/expo+json,application/json'
  });
  let fullManifestUrl = url.replace('exp://', 'https://');
  let bundleUrl = manifest.bundleUrl;

  let shellPath = _path().default.join(exponentDirectory(workingDir), 'android-shell-app');

  await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app-manifest.json'));
  await _fsExtra().default.writeFileSync(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app-manifest.json'), JSON.stringify(manifest));
  await _fsExtra().default.remove(_path().default.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle'));
  await saveUrlToPathAsync(bundleUrl, _path().default.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle'));
  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://shell-app-manifest.json", "application/json"));
    embeddedResponses.add(new Constants.EmbeddedResponse("${bundleUrl}", "assets://shell-app.bundle", "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 getRemoteOrLocalUrl(manifest, key, isDetached) {
  // in detached apps, `manifest` is actually just app.json, so there are no remote url fields
  // we should return a local url starting with file:// instead
  if (isDetached) {
    return _lodash().default.get(manifest, key);
  }

  return _lodash().default.get(manifest, `${key}Url`);
}

function backgroundImagesForApp(shellPath, manifest, isDetached) {
  // returns an array like:
  // [
  //   {url: 'urlToDownload', path: 'pathToSaveTo'},
  //   {url: 'anotherURlToDownload', path: 'anotherPathToSaveTo'},
  // ]
  let basePath = _path().default.join(shellPath, 'app', 'src', 'main', 'res');

  if (_lodash().default.get(manifest, 'android.splash')) {
    const splash = _lodash().default.get(manifest, 'android.splash');

    const results = _lodash().default.reduce(imageKeys, function (acc, imageKey) {
      let url = getRemoteOrLocalUrl(splash, imageKey, isDetached);

      if (url) {
        acc.push({
          url,
          path: _path().default.join(basePath, `drawable-${imageKey}`, 'shell_launch_background_image.png')
        });
      }

      return acc;
    }, []); // No splash screen images declared in 'android.splash' configuration, proceed to general one


    if (results.length !== 0) {
      return results;
    }
  }

  let url = getRemoteOrLocalUrl(manifest, 'splash.image', isDetached);

  if (url) {
    return [{
      url,
      path: _path().default.join(basePath, 'drawable-xxxhdpi', 'shell_launch_background_image.png')
    }];
  }

  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 = manifest.android && manifest.android.splash && manifest.android.splash.resizeMode || manifest.splash && manifest.splash.resizeMode;
  return resizeMode && (parseSdkMajorVersion(sdkVersion) >= 33 ? resizeMode === 'contain' || resizeMode === 'cover' : resizeMode === 'cover');
}

async function copyInitialShellAppFilesAsync(androidSrcPath, shellPath, isDetached, sdkVersion) {
  if (androidSrcPath && !isDetached) {
    // check if Android template files exist
    // since we take out the prebuild step later on
    // and we should have generated those files earlier
    await spawnAsyncThrowError('../../tools-public/check-dynamic-macros-android.sh', [], {
      pipeToLogger: true,
      loggerFields: {
        buildPhase: 'confirming that dynamic macros exist'
      },
      cwd: _path().default.join(androidSrcPath, 'app'),
      env: process.env
    });
  }

  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('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
  } = args;
  const exponentDir = exponentDirectory(workingDir);

  let androidSrcPath = _path().default.join(exponentDir, 'android');

  let 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) {
    let 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
    };
  }

  let buildFlags = _StandaloneBuildFlags().default.createAndroid(configuration, androidBuildConfiguration);

  let context = _StandaloneContext().default.createServiceContext(androidSrcPath, null, manifest, privateConfig,
  /* testEnvironment */
  'none', buildFlags, url, releaseChannel);

  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);
  }
};

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');
  }
}

async function runShellAppModificationsAsync(context, sdkVersion, buildMode) {
  const fnLogger = _Logger().default.withFields({
    buildPhase: 'running shell app modifications'
  });

  let shellPath = shellPathForContext(context);
  let url = context.published.url;
  let manifest = context.config; // manifest or app.json

  let 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;

  if (!context.data.privateConfig) {
    fnLogger.info('No config file specified.');
  }

  let fullManifestUrl = url.replace('exp://', 'https://');
  let versionCode = 1;
  let 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).');
  }

  let name = manifest.name;
  let scheme = manifest.scheme || manifest.detach && manifest.detach.scheme;
  let bundleUrl = manifest.bundleUrl;
  let isFullManifest = !!bundleUrl;
  let version = manifest.version ? manifest.version : '0.0.0';
  let backgroundImages = backgroundImagesForApp(shellPath, manifest, isRunningInUserContext);
  let splashBackgroundColor = getSplashScreenBackgroundColor(manifest);
  let updatesDisabled = manifest.updates && manifest.updates.enabled === false; // 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) {
    let 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);

    if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 33) {
      let settingsGradle = _path().default.join(shellPath, 'settings.gradle');

      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 regexFileAsync('TEMPLATE_INITIAL_URL', url, _path().default.join(shellPath, 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'MainActivity.java'));

    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 (parseSdkMajorVersion(sdkVersion) < 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')); // Push notifications

  await regexFileAsync('"package_name": "host.exp.exponent"', `"package_name": "${javaPackage}"`, _path().default.join(shellPath, 'app', 'google-services.json')); // TODO: actually use the correct file
  // 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 (parseSdkMajorVersion(sdkVersion) < 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'));
  } // 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'));
  } // In SDK32 this field got removed from AppConstants


  if (parseSdkMajorVersion(sdkVersion) < 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'));
  } // App name


  await regexFileAsync('"app_name">Expo', `"app_name">${xmlWeirdAndroidEscape(name)}`, _path().default.join(shellPath, 'app', 'src', 'main', 'res', 'values', 'strings.xml')); // Splash Screen background color

  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 = _lodash().default.get(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 (scheme) {
    const searchLine = isDetached ? '<!-- ADD DETACH SCHEME HERE -->' : '<!-- ADD SHELL SCHEME HERE -->';
    await regexFileAsync(searchLine, `<intent-filter>
        <data android:scheme="${scheme}"/>

        <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 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 = ['android.permission.ACCESS_COARSE_LOCATION', 'android.permission.ACCESS_FINE_LOCATION', 'android.permission.CAMERA', 'android.permission.MANAGE_DOCUMENTS', 'android.permission.READ_CONTACTS', 'android.permission.WRITE_CONTACTS', 'android.permission.READ_CALENDAR', 'android.permission.WRITE_CALENDAR', 'android.permission.READ_EXTERNAL_STORAGE', 'android.permission.READ_INTERNAL_STORAGE', 'android.permission.READ_PHONE_STATE', 'android.permission.RECORD_AUDIO', 'android.permission.USE_FINGERPRINT', 'android.permission.VIBRATE', 'android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_SMS', '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', 'shell-app-manifest.json'), JSON.stringify(manifest));
    await saveUrlToPathAsync(bundleUrl, _path().default.join(shellPath, 'app', 'src', 'main', 'assets', 'shell-app.bundle'));
    await regexFileAsync('// START EMBEDDED RESPONSES', `
      // START EMBEDDED RESPONSES
      embeddedResponses.add(new Constants.EmbeddedResponse("${fullManifestUrl}", "assets://shell-app-manifest.json", "application/json"));
      embeddedResponses.add(new Constants.EmbeddedResponse("${bundleUrl}", "assets://shell-app.bundle", "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); // Splash Background

  if (backgroundImages && backgroundImages.length > 0) {
    // Delete the placeholder images
    (await (0, _globby().default)(['**/shell_launch_background_image.png'], {
      cwd: _path().default.join(shellPath, 'app', 'src', 'main', 'res'),
      absolute: true
    })).forEach(filePath => {
      _fsExtra().default.removeSync(filePath);
    });
    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 = '';
  let privateConfig = context.data.privateConfig;

  if (privateConfig) {
    let branch = privateConfig.branch;
    let fabric = privateConfig.fabric;
    let googleMaps = privateConfig.googleMaps;
    let googleSignIn = privateConfig.googleSignIn;
    let googleMobileAdsAppId = privateConfig.googleMobileAdsAppId; // 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'));
    } // 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'));
  } // 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'));
  } // 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'));
}

async function buildShellAppAsync(context, sdkVersion, buildType, buildMode) {
  let 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) >= 33) {
      gradleBuildCommand = `bundle${debugOrRelease}`;
      outputPath = _path().default.join(outputDirPath, debugOrReleaseL, `app.aab`);
    } else if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32) {
      gradleBuildCommand = `bundle${devOrProd}Kernel${debugOrRelease}`;
      outputPath = _path().default.join(outputDirPath, `${devOrProdL}Kernel${debugOrRelease}`, `app.aab`);
    } else {
      // gradleBuildCommand = `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 = `assemble${debugOrRelease}`;
      outputPath = _path().default.join(outputDirPath, debugOrReleaseL, `app-${debugOrReleaseL}.apk`);
    } else if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32) {
      gradleBuildCommand = `assemble${devOrProd}Kernel${debugOrRelease}`;
      outputPath = _path().default.join(outputDirPath, `${devOrProdL}Kernel`, debugOrReleaseL, `app-${devOrProdL}Kernel-${debugOrReleaseL}.apk`);
    } else {
      gradleBuildCommand = `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 = [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,
        ANDROID_KEYSTORE_PATH: androidBuildConfiguration.keystore,
        ANDROID_KEYSTORE_PASSWORD: androidBuildConfiguration.keystorePassword
      }
    });

    if (ExponentTools().parseSdkMajorVersion(sdkVersion) >= 32) {
      await _fsExtra().default.copy(outputPath, shellFile); // -c means "only verify"

      await spawnAsync(`zipalign`, ['-c', '-v', '4', shellFile], {
        pipeToLogger: true,
        loggerFields: {
          buildPhase: 'verifying apk alignment'
        }
      });
    } else {
      await _fsExtra().default.copy(outputPath, shellUnalignedFile);
      await spawnAsync(`jarsigner`, ['-verbose', '-sigalg', 'SHA1withRSA', '-digestalg', 'SHA1', '-storepass', androidBuildConfiguration.keystorePassword, '-keypass', androidBuildConfiguration.keyPassword, '-keystore', androidBuildConfiguration.keystore, shellUnalignedFile, androidBuildConfiguration.keyAlias], {
        pipeToLogger: true,
        loggerFields: {
          buildPhase: 'signing created apk'
        }
      });
      await spawnAsync(`zipalign`, ['-v', '4', shellUnalignedFile, shellFile], {
        pipeToLogger: true,
        loggerFields: {
          buildPhase: 'verifying apk alignment'
        }
      });
      await ExponentTools().removeIfExists(shellUnalignedFile);
    }

    await spawnAsync(`jarsigner`, ['-verify', '-verbose', '-certs', '-keystore', androidBuildConfiguration.keystore, shellFile], {
      pipeToLogger: true,
      loggerFields: {
        buildPhase: 'verifying apk'
      }
    });
    await _fsExtra().default.copy(shellFile, androidBuildConfiguration.outputFile || `/tmp/shell-signed.${ext}`);
    await ExponentTools().removeIfExists(shellFile);
  } else {
    await spawnAsyncThrowError(`./gradlew`, [gradleBuildCommand], {
      pipeToLogger: true,
      loggerFields: {
        buildPhase: 'running gradle'
      },
      cwd: shellPath
    });
    await _fsExtra().default.copy(outputPath, _lodash().default.get(context, 'build.android.outputFile') || `/tmp/shell-debug.${ext}`);
    await ExponentTools().removeIfExists(outputPath);
  }
}

function addDetachedConfigToExp(exp, context) {
  if (context.type !== 'user') {
    console.warn(`Tried to modify exp for a non-user StandaloneContext, ignoring`);
    return exp;
  }

  let shellPath = shellPathForContext(context);

  let assetsDirectory = _path().default.join(shellPath, 'app', 'src', 'main', 'assets');

  exp.android.publishBundlePath = _path().default.relative(context.data.projectPath, _path().default.join(assetsDirectory, 'shell-app.bundle'));
  exp.android.publishManifestPath = _path().default.relative(context.data.projectPath, _path().default.join(assetsDirectory, 'shell-app-manifest.json'));
  return exp;
}
/**

Some files in `android` directory have the following patterns of code:

```
// WHEN_PREPARING_SHELL_REMOVE_FROM_HERE

// BEGIN_SDK_30
some SDK 30-specific code
// END_SDK_30

// BEGIN_SDK_29
some SDK 29-specific code
// END_SDK_29

...

// WHEN_PREPARING_SHELL_REMOVE_TO_HERE
```

The following function replaces all `BEGIN_SDK_XX` with `REMOVE_TO_HERE`
and all `END_SDK_XX` with `REMOVE_FROM_HERE`, so after the change the code above would read:

```
// WHEN_PREPARING_SHELL_REMOVE_FROM_HERE

// WHEN_PREPARING_SHELL_REMOVE_TO_HERE       <--- changed
some SDK 30-specific code
// WHEN_PREPARING_SHELL_REMOVE_FROM_HERE     <--- changed

// BEGIN_SDK_29
some SDK 29-specific code
// END_SDK_29

...

// WHEN_PREPARING_SHELL_REMOVE_TO_HERE
```

This allows us to use `deleteLinesInFileAsync` function to remove obsolete SDKs code easily.

 */


const removeInvalidSdkLinesWhenPreparingShell = async (majorSdkVersion, filePath) => {
  await regexFileAsync(new RegExp(`BEGIN_SDK_${majorSdkVersion}`, 'g'), `WHEN_PREPARING_SHELL_REMOVE_TO_HERE`, filePath);
  await regexFileAsync(new RegExp(`END_SDK_${majorSdkVersion}`, 'g'), `WHEN_PREPARING_SHELL_REMOVE_FROM_HERE`, filePath);
  await deleteLinesInFileAsync(/WHEN_PREPARING_SHELL_REMOVE_FROM_HERE/g, 'WHEN_PREPARING_SHELL_REMOVE_TO_HERE', filePath);
};

async function removeObsoleteSdks(shellPath, requiredSdkVersion) {
  const filePathsToTransform = {
    // Remove obsolete `expoview-abiXX_X_X` dependencies
    appBuildGradle: _path().default.join(shellPath, 'app/build.gradle'),
    // Remove obsolete `host.exp.exponent:reactandroid:XX.X.X` dependencies from expoview
    expoviewBuildGradle: _path().default.join(shellPath, 'expoview/build.gradle'),
    // Remove obsolete includeUnimodulesProjects
    settingsBuildGradle: _path().default.join(shellPath, 'settings.gradle'),
    // Remove no-longer-valid interfaces from MultipleVersionReactNativeActivity
    multipleVersionReactNativeActivity: _path().default.join(shellPath, 'expoview/src/main/java/host/exp/exponent/experience/MultipleVersionReactNativeActivity.java'),
    // Remove invalid ABI versions from Constants
    constants: _path().default.join(shellPath, 'expoview/src/main/java/host/exp/exponent/Constants.java'),
    // Remove non-existent DevSettingsActivities
    appAndroidManifest: _path().default.join(shellPath, 'app/src/main/AndroidManifest.xml')
  };
  const majorSdkVersion = parseSdkMajorVersion(requiredSdkVersion); // The single SDK change will happen when transitioning from SDK 30 to 31.
  // Since SDK 31 there will be no versioned ABIs in shell applications, only unversioned one.
  // And as such, we will treat the unversioned ABI as the SDK one by assigning TEMPORARY_ABI_VERSION.

  const effectiveSdkVersion = majorSdkVersion > 30 ? 'UNVERSIONED' : `${majorSdkVersion}`;

  if (effectiveSdkVersion === 'UNVERSIONED') {
    await regexFileAsync('TEMPORARY_ABI_VERSION = null;', `TEMPORARY_ABI_VERSION = "${requiredSdkVersion}";`, filePathsToTransform.constants);
  }

  await Promise.all(Object.values(filePathsToTransform).map(filePath => removeInvalidSdkLinesWhenPreparingShell(effectiveSdkVersion, filePath)));
}

async function prepareEnabledModules(shellPath, modules) {
  const enabledModulesDir = _path().default.join(shellPath, 'enabled-modules');

  const packagesDir = _path().default.join(shellPath, '..', 'packages');

  await _fsExtra().default.remove(enabledModulesDir);

  if (!modules) {
    await _fsExtra().default.ensureSymlink(packagesDir, enabledModulesDir);
  } else {
    await _fsExtra().default.mkdirp(enabledModulesDir);
    await Promise.all(modules.map(mod => _fsExtra().default.ensureSymlink(_path().default.join(packagesDir, mod.dirname), _path().default.join(enabledModulesDir, mod.dirname))));
  }
}
//# sourceMappingURL=../__sourcemaps__/detach/AndroidShellApp.js.map