import { promises } from 'fs'; import path from 'path'; import { ForwardedBaseModOptions, provider, withGeneratedBaseMods } from './createBaseMod'; import { ExportedConfig, ModConfig } from '../Plugin.types'; import { Colors, Manifest, Paths, Properties, Resources, Strings, Styles } from '../android'; import { AndroidManifest } from '../android/Manifest'; import { parseXMLAsync, writeXMLAsync } from '../utils/XML'; import { reverseSortString, sortObject, sortObjWithOrder } from '../utils/sortObject'; const { readFile, writeFile } = promises; type AndroidModName = keyof Required['android']; function getAndroidManifestTemplate(config: ExportedConfig) { // Keep in sync with https://github.com/expo/expo/blob/master/templates/expo-template-bare-minimum/android/app/src/main/AndroidManifest.xml // TODO: Read from remote template when possible return parseXMLAsync(` `) as Promise; } export function sortAndroidManifest(obj: AndroidManifest) { if (obj.manifest) { // Reverse sort so application is last and permissions are first obj.manifest = sortObject(obj.manifest, reverseSortString); if (Array.isArray(obj.manifest['uses-permission'])) { // Sort permissions alphabetically obj.manifest['uses-permission'].sort((a, b) => { if (a.$['android:name'] < b.$['android:name']) return -1; if (a.$['android:name'] > b.$['android:name']) return 1; return 0; }); } if (Array.isArray(obj.manifest.application)) { // reverse sort applications so activity is towards the end and meta-data is towards the front. obj.manifest.application = obj.manifest.application.map((application) => { application = sortObjWithOrder(application, ['meta-data', 'service', 'activity']); if (Array.isArray(application['meta-data'])) { // Sort metadata alphabetically application['meta-data'].sort((a, b) => { if (a.$['android:name'] < b.$['android:name']) return -1; if (a.$['android:name'] > b.$['android:name']) return 1; return 0; }); } return application; }); } } return obj; } const defaultProviders = { dangerous: provider({ getFilePath() { return ''; }, async read() { return { filePath: '', modResults: {} }; }, async write() {}, }), // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` manifest: provider({ isIntrospective: true, getFilePath({ modRequest: { platformProjectRoot } }) { return path.join(platformProjectRoot, 'app/src/main/AndroidManifest.xml'); }, async read(filePath, config) { try { return await Manifest.readAndroidManifestAsync(filePath); } catch (error: any) { if (!config.modRequest.introspect) { throw error; } } return await getAndroidManifestTemplate(config); }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await Manifest.writeAndroidManifestAsync(filePath, sortAndroidManifest(modResults)); }, }), // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` gradleProperties: provider({ isIntrospective: true, getFilePath({ modRequest: { platformProjectRoot } }) { return path.join(platformProjectRoot, 'gradle.properties'); }, async read(filePath, config) { try { return await Properties.parsePropertiesFile(await readFile(filePath, 'utf8')); } catch (error) { if (!config.modRequest.introspect) { throw error; } } return []; }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await writeFile(filePath, Properties.propertiesListToString(modResults)); }, }), // Append a rule to supply strings.xml data to mods on `mods.android.strings` strings: provider({ isIntrospective: true, async getFilePath({ modRequest: { projectRoot, introspect } }) { try { return await Strings.getProjectStringsXMLPathAsync(projectRoot); } catch (error: any) { if (!introspect) { throw error; } } return ''; }, async read(filePath, config) { try { return await Resources.readResourcesXMLAsync({ path: filePath }); } catch (error) { if (!config.modRequest.introspect) { throw error; } } return { resources: {} }; }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await writeXMLAsync({ path: filePath, xml: modResults }); }, }), colors: provider({ isIntrospective: true, async getFilePath({ modRequest: { projectRoot, introspect } }) { try { return await Colors.getProjectColorsXMLPathAsync(projectRoot); } catch (error: any) { if (!introspect) { throw error; } } return ''; }, async read(filePath, { modRequest: { introspect } }) { try { return await Resources.readResourcesXMLAsync({ path: filePath }); } catch (error: any) { if (!introspect) { throw error; } } return { resources: {} }; }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await writeXMLAsync({ path: filePath, xml: modResults }); }, }), colorsNight: provider({ isIntrospective: true, async getFilePath({ modRequest: { projectRoot, introspect } }) { try { return await Colors.getProjectColorsXMLPathAsync(projectRoot, { kind: 'values-night' }); } catch (error: any) { if (!introspect) { throw error; } } return ''; }, async read(filePath, config) { try { return await Resources.readResourcesXMLAsync({ path: filePath }); } catch (error: any) { if (!config.modRequest.introspect) { throw error; } } return { resources: {} }; }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await writeXMLAsync({ path: filePath, xml: modResults }); }, }), styles: provider({ isIntrospective: true, async getFilePath({ modRequest: { projectRoot, introspect } }) { try { return await Styles.getProjectStylesXMLPathAsync(projectRoot); } catch (error: any) { if (!introspect) { throw error; } } return ''; }, async read(filePath, config) { let styles: Resources.ResourceXML = { resources: {} }; try { // Adds support for `tools:x` styles = await Resources.readResourcesXMLAsync({ path: filePath, fallback: ``, }); } catch (error: any) { if (!config.modRequest.introspect) { throw error; } } // Ensure support for tools is added... if (!styles.resources.$) { styles.resources.$ = {}; } if (!styles.resources.$?.['xmlns:tools']) { styles.resources.$['xmlns:tools'] = 'http://schemas.android.com/tools'; } return styles; }, async write(filePath, { modResults, modRequest: { introspect } }) { if (introspect) return; await writeXMLAsync({ path: filePath, xml: modResults }); }, }), projectBuildGradle: provider({ getFilePath({ modRequest: { projectRoot } }) { return Paths.getProjectBuildGradleFilePath(projectRoot); }, async read(filePath) { return Paths.getFileInfo(filePath); }, async write(filePath, { modResults: { contents } }) { await writeFile(filePath, contents); }, }), settingsGradle: provider({ getFilePath({ modRequest: { projectRoot } }) { return Paths.getSettingsGradleFilePath(projectRoot); }, async read(filePath) { return Paths.getFileInfo(filePath); }, async write(filePath, { modResults: { contents } }) { await writeFile(filePath, contents); }, }), appBuildGradle: provider({ getFilePath({ modRequest: { projectRoot } }) { return Paths.getAppBuildGradleFilePath(projectRoot); }, async read(filePath) { return Paths.getFileInfo(filePath); }, async write(filePath, { modResults: { contents } }) { await writeFile(filePath, contents); }, }), mainActivity: provider({ getFilePath({ modRequest: { projectRoot } }) { return Paths.getProjectFilePath(projectRoot, 'MainActivity'); }, async read(filePath) { return Paths.getFileInfo(filePath); }, async write(filePath, { modResults: { contents } }) { await writeFile(filePath, contents); }, }), mainApplication: provider({ getFilePath({ modRequest: { projectRoot } }) { return Paths.getProjectFilePath(projectRoot, 'MainApplication'); }, async read(filePath) { return Paths.getFileInfo(filePath); }, async write(filePath, { modResults: { contents } }) { await writeFile(filePath, contents); }, }), }; type AndroidDefaultProviders = typeof defaultProviders; export function withAndroidBaseMods( config: ExportedConfig, { providers, ...props }: ForwardedBaseModOptions & { providers?: Partial } = {} ): ExportedConfig { return withGeneratedBaseMods(config, { ...props, platform: 'android', providers: providers ?? getAndroidModFileProviders(), }); } export function getAndroidModFileProviders() { return defaultProviders; }