1082815dcSEvan Baconimport { promises } from 'fs'; 2082815dcSEvan Baconimport path from 'path'; 3082815dcSEvan Bacon 4*8a424bebSJames Ideimport { ForwardedBaseModOptions, provider, withGeneratedBaseMods } from './createBaseMod'; 5082815dcSEvan Baconimport { ExportedConfig, ModConfig } from '../Plugin.types'; 6082815dcSEvan Baconimport { Colors, Manifest, Paths, Properties, Resources, Strings, Styles } from '../android'; 7082815dcSEvan Baconimport { AndroidManifest } from '../android/Manifest'; 8082815dcSEvan Baconimport { parseXMLAsync, writeXMLAsync } from '../utils/XML'; 9082815dcSEvan Baconimport { reverseSortString, sortObject, sortObjWithOrder } from '../utils/sortObject'; 10082815dcSEvan Bacon 11082815dcSEvan Baconconst { readFile, writeFile } = promises; 12082815dcSEvan Bacon 13082815dcSEvan Bacontype AndroidModName = keyof Required<ModConfig>['android']; 14082815dcSEvan Bacon 15082815dcSEvan Baconfunction getAndroidManifestTemplate(config: ExportedConfig) { 16082815dcSEvan Bacon // Keep in sync with https://github.com/expo/expo/blob/master/templates/expo-template-bare-minimum/android/app/src/main/AndroidManifest.xml 17082815dcSEvan Bacon // TODO: Read from remote template when possible 18082815dcSEvan Bacon return parseXMLAsync(` 19082815dcSEvan Bacon <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${ 20082815dcSEvan Bacon config.android?.package ?? 'com.placeholder.appid' 21082815dcSEvan Bacon }"> 22082815dcSEvan Bacon 23082815dcSEvan Bacon <uses-permission android:name="android.permission.INTERNET"/> 24082815dcSEvan Bacon <!-- OPTIONAL PERMISSIONS, REMOVE WHATEVER YOU DO NOT NEED --> 25082815dcSEvan Bacon <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 26082815dcSEvan Bacon <!-- These require runtime permissions on M --> 27082815dcSEvan Bacon <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 28082815dcSEvan Bacon <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 29082815dcSEvan Bacon <!-- END OPTIONAL PERMISSIONS --> 30082815dcSEvan Bacon 31082815dcSEvan Bacon <queries> 32082815dcSEvan Bacon <!-- Support checking for http(s) links via the Linking API --> 33082815dcSEvan Bacon <intent> 34082815dcSEvan Bacon <action android:name="android.intent.action.VIEW" /> 35082815dcSEvan Bacon <category android:name="android.intent.category.BROWSABLE" /> 36082815dcSEvan Bacon <data android:scheme="https" /> 37082815dcSEvan Bacon </intent> 38082815dcSEvan Bacon </queries> 39082815dcSEvan Bacon 40082815dcSEvan Bacon <application 41082815dcSEvan Bacon android:name=".MainApplication" 42082815dcSEvan Bacon android:label="@string/app_name" 43082815dcSEvan Bacon android:icon="@mipmap/ic_launcher" 44082815dcSEvan Bacon android:roundIcon="@mipmap/ic_launcher_round" 45082815dcSEvan Bacon android:allowBackup="false" 46082815dcSEvan Bacon android:theme="@style/AppTheme" 47082815dcSEvan Bacon android:usesCleartextTraffic="true" 48082815dcSEvan Bacon > 49082815dcSEvan Bacon <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="YOUR-APP-URL-HERE"/> 50082815dcSEvan Bacon <meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="YOUR-APP-SDK-VERSION-HERE"/> 51082815dcSEvan Bacon <activity 52082815dcSEvan Bacon android:name=".MainActivity" 53082815dcSEvan Bacon android:label="@string/app_name" 54082815dcSEvan Bacon android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" 55082815dcSEvan Bacon android:launchMode="singleTask" 56082815dcSEvan Bacon android:windowSoftInputMode="adjustResize" 57082815dcSEvan Bacon android:theme="@style/Theme.App.SplashScreen" 58082815dcSEvan Bacon > 59082815dcSEvan Bacon <intent-filter> 60082815dcSEvan Bacon <action android:name="android.intent.action.MAIN"/> 61082815dcSEvan Bacon <category android:name="android.intent.category.LAUNCHER"/> 62082815dcSEvan Bacon </intent-filter> 63082815dcSEvan Bacon </activity> 64082815dcSEvan Bacon <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/> 65082815dcSEvan Bacon </application> 66082815dcSEvan Bacon </manifest> 67082815dcSEvan Bacon `) as Promise<AndroidManifest>; 68082815dcSEvan Bacon} 69082815dcSEvan Bacon 70082815dcSEvan Baconexport function sortAndroidManifest(obj: AndroidManifest) { 71082815dcSEvan Bacon if (obj.manifest) { 72082815dcSEvan Bacon // Reverse sort so application is last and permissions are first 73082815dcSEvan Bacon obj.manifest = sortObject(obj.manifest, reverseSortString); 74082815dcSEvan Bacon 75082815dcSEvan Bacon if (Array.isArray(obj.manifest['uses-permission'])) { 76082815dcSEvan Bacon // Sort permissions alphabetically 77082815dcSEvan Bacon obj.manifest['uses-permission'].sort((a, b) => { 78082815dcSEvan Bacon if (a.$['android:name'] < b.$['android:name']) return -1; 79082815dcSEvan Bacon if (a.$['android:name'] > b.$['android:name']) return 1; 80082815dcSEvan Bacon return 0; 81082815dcSEvan Bacon }); 82082815dcSEvan Bacon } 83082815dcSEvan Bacon 84082815dcSEvan Bacon if (Array.isArray(obj.manifest.application)) { 85082815dcSEvan Bacon // reverse sort applications so activity is towards the end and meta-data is towards the front. 86082815dcSEvan Bacon obj.manifest.application = obj.manifest.application.map((application) => { 87082815dcSEvan Bacon application = sortObjWithOrder(application, ['meta-data', 'service', 'activity']); 88082815dcSEvan Bacon 89082815dcSEvan Bacon if (Array.isArray(application['meta-data'])) { 90082815dcSEvan Bacon // Sort metadata alphabetically 91082815dcSEvan Bacon application['meta-data'].sort((a, b) => { 92082815dcSEvan Bacon if (a.$['android:name'] < b.$['android:name']) return -1; 93082815dcSEvan Bacon if (a.$['android:name'] > b.$['android:name']) return 1; 94082815dcSEvan Bacon return 0; 95082815dcSEvan Bacon }); 96082815dcSEvan Bacon } 97082815dcSEvan Bacon return application; 98082815dcSEvan Bacon }); 99082815dcSEvan Bacon } 100082815dcSEvan Bacon } 101082815dcSEvan Bacon return obj; 102082815dcSEvan Bacon} 103082815dcSEvan Bacon 104082815dcSEvan Baconconst defaultProviders = { 105082815dcSEvan Bacon dangerous: provider<unknown>({ 106082815dcSEvan Bacon getFilePath() { 107082815dcSEvan Bacon return ''; 108082815dcSEvan Bacon }, 109082815dcSEvan Bacon async read() { 110082815dcSEvan Bacon return { filePath: '', modResults: {} }; 111082815dcSEvan Bacon }, 112082815dcSEvan Bacon async write() {}, 113082815dcSEvan Bacon }), 114082815dcSEvan Bacon 115082815dcSEvan Bacon // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` 116082815dcSEvan Bacon manifest: provider<Manifest.AndroidManifest>({ 117082815dcSEvan Bacon isIntrospective: true, 118082815dcSEvan Bacon getFilePath({ modRequest: { platformProjectRoot } }) { 119082815dcSEvan Bacon return path.join(platformProjectRoot, 'app/src/main/AndroidManifest.xml'); 120082815dcSEvan Bacon }, 121082815dcSEvan Bacon async read(filePath, config) { 122082815dcSEvan Bacon try { 123082815dcSEvan Bacon return await Manifest.readAndroidManifestAsync(filePath); 124082815dcSEvan Bacon } catch (error: any) { 125082815dcSEvan Bacon if (!config.modRequest.introspect) { 126082815dcSEvan Bacon throw error; 127082815dcSEvan Bacon } 128082815dcSEvan Bacon } 129082815dcSEvan Bacon return await getAndroidManifestTemplate(config); 130082815dcSEvan Bacon }, 131082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 132082815dcSEvan Bacon if (introspect) return; 133082815dcSEvan Bacon await Manifest.writeAndroidManifestAsync(filePath, sortAndroidManifest(modResults)); 134082815dcSEvan Bacon }, 135082815dcSEvan Bacon }), 136082815dcSEvan Bacon 137082815dcSEvan Bacon // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` 138082815dcSEvan Bacon gradleProperties: provider<Properties.PropertiesItem[]>({ 139082815dcSEvan Bacon isIntrospective: true, 140082815dcSEvan Bacon 141082815dcSEvan Bacon getFilePath({ modRequest: { platformProjectRoot } }) { 142082815dcSEvan Bacon return path.join(platformProjectRoot, 'gradle.properties'); 143082815dcSEvan Bacon }, 144082815dcSEvan Bacon async read(filePath, config) { 145082815dcSEvan Bacon try { 146082815dcSEvan Bacon return await Properties.parsePropertiesFile(await readFile(filePath, 'utf8')); 147082815dcSEvan Bacon } catch (error) { 148082815dcSEvan Bacon if (!config.modRequest.introspect) { 149082815dcSEvan Bacon throw error; 150082815dcSEvan Bacon } 151082815dcSEvan Bacon } 152082815dcSEvan Bacon return []; 153082815dcSEvan Bacon }, 154082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 155082815dcSEvan Bacon if (introspect) return; 156082815dcSEvan Bacon await writeFile(filePath, Properties.propertiesListToString(modResults)); 157082815dcSEvan Bacon }, 158082815dcSEvan Bacon }), 159082815dcSEvan Bacon 160082815dcSEvan Bacon // Append a rule to supply strings.xml data to mods on `mods.android.strings` 161082815dcSEvan Bacon strings: provider<Resources.ResourceXML>({ 162082815dcSEvan Bacon isIntrospective: true, 163082815dcSEvan Bacon 164082815dcSEvan Bacon async getFilePath({ modRequest: { projectRoot, introspect } }) { 165082815dcSEvan Bacon try { 166082815dcSEvan Bacon return await Strings.getProjectStringsXMLPathAsync(projectRoot); 167082815dcSEvan Bacon } catch (error: any) { 168082815dcSEvan Bacon if (!introspect) { 169082815dcSEvan Bacon throw error; 170082815dcSEvan Bacon } 171082815dcSEvan Bacon } 172082815dcSEvan Bacon return ''; 173082815dcSEvan Bacon }, 174082815dcSEvan Bacon 175082815dcSEvan Bacon async read(filePath, config) { 176082815dcSEvan Bacon try { 177082815dcSEvan Bacon return await Resources.readResourcesXMLAsync({ path: filePath }); 178082815dcSEvan Bacon } catch (error) { 179082815dcSEvan Bacon if (!config.modRequest.introspect) { 180082815dcSEvan Bacon throw error; 181082815dcSEvan Bacon } 182082815dcSEvan Bacon } 183082815dcSEvan Bacon return { resources: {} }; 184082815dcSEvan Bacon }, 185082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 186082815dcSEvan Bacon if (introspect) return; 187082815dcSEvan Bacon await writeXMLAsync({ path: filePath, xml: modResults }); 188082815dcSEvan Bacon }, 189082815dcSEvan Bacon }), 190082815dcSEvan Bacon 191082815dcSEvan Bacon colors: provider<Resources.ResourceXML>({ 192082815dcSEvan Bacon isIntrospective: true, 193082815dcSEvan Bacon 194082815dcSEvan Bacon async getFilePath({ modRequest: { projectRoot, introspect } }) { 195082815dcSEvan Bacon try { 196082815dcSEvan Bacon return await Colors.getProjectColorsXMLPathAsync(projectRoot); 197082815dcSEvan Bacon } catch (error: any) { 198082815dcSEvan Bacon if (!introspect) { 199082815dcSEvan Bacon throw error; 200082815dcSEvan Bacon } 201082815dcSEvan Bacon } 202082815dcSEvan Bacon return ''; 203082815dcSEvan Bacon }, 204082815dcSEvan Bacon 205082815dcSEvan Bacon async read(filePath, { modRequest: { introspect } }) { 206082815dcSEvan Bacon try { 207082815dcSEvan Bacon return await Resources.readResourcesXMLAsync({ path: filePath }); 208082815dcSEvan Bacon } catch (error: any) { 209082815dcSEvan Bacon if (!introspect) { 210082815dcSEvan Bacon throw error; 211082815dcSEvan Bacon } 212082815dcSEvan Bacon } 213082815dcSEvan Bacon return { resources: {} }; 214082815dcSEvan Bacon }, 215082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 216082815dcSEvan Bacon if (introspect) return; 217082815dcSEvan Bacon await writeXMLAsync({ path: filePath, xml: modResults }); 218082815dcSEvan Bacon }, 219082815dcSEvan Bacon }), 220082815dcSEvan Bacon 221082815dcSEvan Bacon colorsNight: provider<Resources.ResourceXML>({ 222082815dcSEvan Bacon isIntrospective: true, 223082815dcSEvan Bacon 224082815dcSEvan Bacon async getFilePath({ modRequest: { projectRoot, introspect } }) { 225082815dcSEvan Bacon try { 226082815dcSEvan Bacon return await Colors.getProjectColorsXMLPathAsync(projectRoot, { kind: 'values-night' }); 227082815dcSEvan Bacon } catch (error: any) { 228082815dcSEvan Bacon if (!introspect) { 229082815dcSEvan Bacon throw error; 230082815dcSEvan Bacon } 231082815dcSEvan Bacon } 232082815dcSEvan Bacon return ''; 233082815dcSEvan Bacon }, 234082815dcSEvan Bacon async read(filePath, config) { 235082815dcSEvan Bacon try { 236082815dcSEvan Bacon return await Resources.readResourcesXMLAsync({ path: filePath }); 237082815dcSEvan Bacon } catch (error: any) { 238082815dcSEvan Bacon if (!config.modRequest.introspect) { 239082815dcSEvan Bacon throw error; 240082815dcSEvan Bacon } 241082815dcSEvan Bacon } 242082815dcSEvan Bacon return { resources: {} }; 243082815dcSEvan Bacon }, 244082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 245082815dcSEvan Bacon if (introspect) return; 246082815dcSEvan Bacon await writeXMLAsync({ path: filePath, xml: modResults }); 247082815dcSEvan Bacon }, 248082815dcSEvan Bacon }), 249082815dcSEvan Bacon 250082815dcSEvan Bacon styles: provider<Resources.ResourceXML>({ 251082815dcSEvan Bacon isIntrospective: true, 252082815dcSEvan Bacon 253082815dcSEvan Bacon async getFilePath({ modRequest: { projectRoot, introspect } }) { 254082815dcSEvan Bacon try { 255082815dcSEvan Bacon return await Styles.getProjectStylesXMLPathAsync(projectRoot); 256082815dcSEvan Bacon } catch (error: any) { 257082815dcSEvan Bacon if (!introspect) { 258082815dcSEvan Bacon throw error; 259082815dcSEvan Bacon } 260082815dcSEvan Bacon } 261082815dcSEvan Bacon return ''; 262082815dcSEvan Bacon }, 263082815dcSEvan Bacon async read(filePath, config) { 264082815dcSEvan Bacon let styles: Resources.ResourceXML = { resources: {} }; 265082815dcSEvan Bacon 266082815dcSEvan Bacon try { 267082815dcSEvan Bacon // Adds support for `tools:x` 268082815dcSEvan Bacon styles = await Resources.readResourcesXMLAsync({ 269082815dcSEvan Bacon path: filePath, 270082815dcSEvan Bacon fallback: `<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"></resources>`, 271082815dcSEvan Bacon }); 272082815dcSEvan Bacon } catch (error: any) { 273082815dcSEvan Bacon if (!config.modRequest.introspect) { 274082815dcSEvan Bacon throw error; 275082815dcSEvan Bacon } 276082815dcSEvan Bacon } 277082815dcSEvan Bacon 278082815dcSEvan Bacon // Ensure support for tools is added... 279082815dcSEvan Bacon if (!styles.resources.$) { 280082815dcSEvan Bacon styles.resources.$ = {}; 281082815dcSEvan Bacon } 282082815dcSEvan Bacon if (!styles.resources.$?.['xmlns:tools']) { 283082815dcSEvan Bacon styles.resources.$['xmlns:tools'] = 'http://schemas.android.com/tools'; 284082815dcSEvan Bacon } 285082815dcSEvan Bacon return styles; 286082815dcSEvan Bacon }, 287082815dcSEvan Bacon async write(filePath, { modResults, modRequest: { introspect } }) { 288082815dcSEvan Bacon if (introspect) return; 289082815dcSEvan Bacon await writeXMLAsync({ path: filePath, xml: modResults }); 290082815dcSEvan Bacon }, 291082815dcSEvan Bacon }), 292082815dcSEvan Bacon 293082815dcSEvan Bacon projectBuildGradle: provider<Paths.GradleProjectFile>({ 294082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 295082815dcSEvan Bacon return Paths.getProjectBuildGradleFilePath(projectRoot); 296082815dcSEvan Bacon }, 297082815dcSEvan Bacon async read(filePath) { 298082815dcSEvan Bacon return Paths.getFileInfo(filePath); 299082815dcSEvan Bacon }, 300082815dcSEvan Bacon async write(filePath, { modResults: { contents } }) { 301082815dcSEvan Bacon await writeFile(filePath, contents); 302082815dcSEvan Bacon }, 303082815dcSEvan Bacon }), 304082815dcSEvan Bacon 305082815dcSEvan Bacon settingsGradle: provider<Paths.GradleProjectFile>({ 306082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 307082815dcSEvan Bacon return Paths.getSettingsGradleFilePath(projectRoot); 308082815dcSEvan Bacon }, 309082815dcSEvan Bacon async read(filePath) { 310082815dcSEvan Bacon return Paths.getFileInfo(filePath); 311082815dcSEvan Bacon }, 312082815dcSEvan Bacon async write(filePath, { modResults: { contents } }) { 313082815dcSEvan Bacon await writeFile(filePath, contents); 314082815dcSEvan Bacon }, 315082815dcSEvan Bacon }), 316082815dcSEvan Bacon 317082815dcSEvan Bacon appBuildGradle: provider<Paths.GradleProjectFile>({ 318082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 319082815dcSEvan Bacon return Paths.getAppBuildGradleFilePath(projectRoot); 320082815dcSEvan Bacon }, 321082815dcSEvan Bacon async read(filePath) { 322082815dcSEvan Bacon return Paths.getFileInfo(filePath); 323082815dcSEvan Bacon }, 324082815dcSEvan Bacon async write(filePath, { modResults: { contents } }) { 325082815dcSEvan Bacon await writeFile(filePath, contents); 326082815dcSEvan Bacon }, 327082815dcSEvan Bacon }), 328082815dcSEvan Bacon 329082815dcSEvan Bacon mainActivity: provider<Paths.ApplicationProjectFile>({ 330082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 331082815dcSEvan Bacon return Paths.getProjectFilePath(projectRoot, 'MainActivity'); 332082815dcSEvan Bacon }, 333082815dcSEvan Bacon async read(filePath) { 334082815dcSEvan Bacon return Paths.getFileInfo(filePath); 335082815dcSEvan Bacon }, 336082815dcSEvan Bacon async write(filePath, { modResults: { contents } }) { 337082815dcSEvan Bacon await writeFile(filePath, contents); 338082815dcSEvan Bacon }, 339082815dcSEvan Bacon }), 340082815dcSEvan Bacon 341082815dcSEvan Bacon mainApplication: provider<Paths.ApplicationProjectFile>({ 342082815dcSEvan Bacon getFilePath({ modRequest: { projectRoot } }) { 343082815dcSEvan Bacon return Paths.getProjectFilePath(projectRoot, 'MainApplication'); 344082815dcSEvan Bacon }, 345082815dcSEvan Bacon async read(filePath) { 346082815dcSEvan Bacon return Paths.getFileInfo(filePath); 347082815dcSEvan Bacon }, 348082815dcSEvan Bacon async write(filePath, { modResults: { contents } }) { 349082815dcSEvan Bacon await writeFile(filePath, contents); 350082815dcSEvan Bacon }, 351082815dcSEvan Bacon }), 352082815dcSEvan Bacon}; 353082815dcSEvan Bacon 354082815dcSEvan Bacontype AndroidDefaultProviders = typeof defaultProviders; 355082815dcSEvan Bacon 356082815dcSEvan Baconexport function withAndroidBaseMods( 357082815dcSEvan Bacon config: ExportedConfig, 358082815dcSEvan Bacon { 359082815dcSEvan Bacon providers, 360082815dcSEvan Bacon ...props 361082815dcSEvan Bacon }: ForwardedBaseModOptions & { providers?: Partial<AndroidDefaultProviders> } = {} 362082815dcSEvan Bacon): ExportedConfig { 363082815dcSEvan Bacon return withGeneratedBaseMods<AndroidModName>(config, { 364082815dcSEvan Bacon ...props, 365082815dcSEvan Bacon platform: 'android', 366082815dcSEvan Bacon providers: providers ?? getAndroidModFileProviders(), 367082815dcSEvan Bacon }); 368082815dcSEvan Bacon} 369082815dcSEvan Bacon 370082815dcSEvan Baconexport function getAndroidModFileProviders() { 371082815dcSEvan Bacon return defaultProviders; 372082815dcSEvan Bacon} 373