1import { promises } from 'fs'; 2import path from 'path'; 3 4import { ForwardedBaseModOptions, provider, withGeneratedBaseMods } from './createBaseMod'; 5import { ExportedConfig, ModConfig } from '../Plugin.types'; 6import { Colors, Manifest, Paths, Properties, Resources, Strings, Styles } from '../android'; 7import { AndroidManifest } from '../android/Manifest'; 8import { parseXMLAsync, writeXMLAsync } from '../utils/XML'; 9import { reverseSortString, sortObject, sortObjWithOrder } from '../utils/sortObject'; 10 11const { readFile, writeFile } = promises; 12 13type AndroidModName = keyof Required<ModConfig>['android']; 14 15function getAndroidManifestTemplate(config: ExportedConfig) { 16 // Keep in sync with https://github.com/expo/expo/blob/master/templates/expo-template-bare-minimum/android/app/src/main/AndroidManifest.xml 17 // TODO: Read from remote template when possible 18 return parseXMLAsync(` 19 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="${ 20 config.android?.package ?? 'com.placeholder.appid' 21 }"> 22 23 <uses-permission android:name="android.permission.INTERNET"/> 24 <!-- OPTIONAL PERMISSIONS, REMOVE WHATEVER YOU DO NOT NEED --> 25 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> 26 <!-- These require runtime permissions on M --> 27 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> 28 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 29 <!-- END OPTIONAL PERMISSIONS --> 30 31 <queries> 32 <!-- Support checking for http(s) links via the Linking API --> 33 <intent> 34 <action android:name="android.intent.action.VIEW" /> 35 <category android:name="android.intent.category.BROWSABLE" /> 36 <data android:scheme="https" /> 37 </intent> 38 </queries> 39 40 <application 41 android:name=".MainApplication" 42 android:label="@string/app_name" 43 android:icon="@mipmap/ic_launcher" 44 android:roundIcon="@mipmap/ic_launcher_round" 45 android:allowBackup="false" 46 android:theme="@style/AppTheme" 47 android:usesCleartextTraffic="true" 48 > 49 <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="YOUR-APP-URL-HERE"/> 50 <meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="YOUR-APP-SDK-VERSION-HERE"/> 51 <activity 52 android:name=".MainActivity" 53 android:label="@string/app_name" 54 android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" 55 android:launchMode="singleTask" 56 android:windowSoftInputMode="adjustResize" 57 android:theme="@style/Theme.App.SplashScreen" 58 > 59 <intent-filter> 60 <action android:name="android.intent.action.MAIN"/> 61 <category android:name="android.intent.category.LAUNCHER"/> 62 </intent-filter> 63 </activity> 64 <activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/> 65 </application> 66 </manifest> 67 `) as Promise<AndroidManifest>; 68} 69 70export function sortAndroidManifest(obj: AndroidManifest) { 71 if (obj.manifest) { 72 // Reverse sort so application is last and permissions are first 73 obj.manifest = sortObject(obj.manifest, reverseSortString); 74 75 if (Array.isArray(obj.manifest['uses-permission'])) { 76 // Sort permissions alphabetically 77 obj.manifest['uses-permission'].sort((a, b) => { 78 if (a.$['android:name'] < b.$['android:name']) return -1; 79 if (a.$['android:name'] > b.$['android:name']) return 1; 80 return 0; 81 }); 82 } 83 84 if (Array.isArray(obj.manifest.application)) { 85 // reverse sort applications so activity is towards the end and meta-data is towards the front. 86 obj.manifest.application = obj.manifest.application.map((application) => { 87 application = sortObjWithOrder(application, ['meta-data', 'service', 'activity']); 88 89 if (Array.isArray(application['meta-data'])) { 90 // Sort metadata alphabetically 91 application['meta-data'].sort((a, b) => { 92 if (a.$['android:name'] < b.$['android:name']) return -1; 93 if (a.$['android:name'] > b.$['android:name']) return 1; 94 return 0; 95 }); 96 } 97 return application; 98 }); 99 } 100 } 101 return obj; 102} 103 104const defaultProviders = { 105 dangerous: provider<unknown>({ 106 getFilePath() { 107 return ''; 108 }, 109 async read() { 110 return { filePath: '', modResults: {} }; 111 }, 112 async write() {}, 113 }), 114 115 // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` 116 manifest: provider<Manifest.AndroidManifest>({ 117 isIntrospective: true, 118 getFilePath({ modRequest: { platformProjectRoot } }) { 119 return path.join(platformProjectRoot, 'app/src/main/AndroidManifest.xml'); 120 }, 121 async read(filePath, config) { 122 try { 123 return await Manifest.readAndroidManifestAsync(filePath); 124 } catch (error: any) { 125 if (!config.modRequest.introspect) { 126 throw error; 127 } 128 } 129 return await getAndroidManifestTemplate(config); 130 }, 131 async write(filePath, { modResults, modRequest: { introspect } }) { 132 if (introspect) return; 133 await Manifest.writeAndroidManifestAsync(filePath, sortAndroidManifest(modResults)); 134 }, 135 }), 136 137 // Append a rule to supply gradle.properties data to mods on `mods.android.gradleProperties` 138 gradleProperties: provider<Properties.PropertiesItem[]>({ 139 isIntrospective: true, 140 141 getFilePath({ modRequest: { platformProjectRoot } }) { 142 return path.join(platformProjectRoot, 'gradle.properties'); 143 }, 144 async read(filePath, config) { 145 try { 146 return await Properties.parsePropertiesFile(await readFile(filePath, 'utf8')); 147 } catch (error) { 148 if (!config.modRequest.introspect) { 149 throw error; 150 } 151 } 152 return []; 153 }, 154 async write(filePath, { modResults, modRequest: { introspect } }) { 155 if (introspect) return; 156 await writeFile(filePath, Properties.propertiesListToString(modResults)); 157 }, 158 }), 159 160 // Append a rule to supply strings.xml data to mods on `mods.android.strings` 161 strings: provider<Resources.ResourceXML>({ 162 isIntrospective: true, 163 164 async getFilePath({ modRequest: { projectRoot, introspect } }) { 165 try { 166 return await Strings.getProjectStringsXMLPathAsync(projectRoot); 167 } catch (error: any) { 168 if (!introspect) { 169 throw error; 170 } 171 } 172 return ''; 173 }, 174 175 async read(filePath, config) { 176 try { 177 return await Resources.readResourcesXMLAsync({ path: filePath }); 178 } catch (error) { 179 if (!config.modRequest.introspect) { 180 throw error; 181 } 182 } 183 return { resources: {} }; 184 }, 185 async write(filePath, { modResults, modRequest: { introspect } }) { 186 if (introspect) return; 187 await writeXMLAsync({ path: filePath, xml: modResults }); 188 }, 189 }), 190 191 colors: provider<Resources.ResourceXML>({ 192 isIntrospective: true, 193 194 async getFilePath({ modRequest: { projectRoot, introspect } }) { 195 try { 196 return await Colors.getProjectColorsXMLPathAsync(projectRoot); 197 } catch (error: any) { 198 if (!introspect) { 199 throw error; 200 } 201 } 202 return ''; 203 }, 204 205 async read(filePath, { modRequest: { introspect } }) { 206 try { 207 return await Resources.readResourcesXMLAsync({ path: filePath }); 208 } catch (error: any) { 209 if (!introspect) { 210 throw error; 211 } 212 } 213 return { resources: {} }; 214 }, 215 async write(filePath, { modResults, modRequest: { introspect } }) { 216 if (introspect) return; 217 await writeXMLAsync({ path: filePath, xml: modResults }); 218 }, 219 }), 220 221 colorsNight: provider<Resources.ResourceXML>({ 222 isIntrospective: true, 223 224 async getFilePath({ modRequest: { projectRoot, introspect } }) { 225 try { 226 return await Colors.getProjectColorsXMLPathAsync(projectRoot, { kind: 'values-night' }); 227 } catch (error: any) { 228 if (!introspect) { 229 throw error; 230 } 231 } 232 return ''; 233 }, 234 async read(filePath, config) { 235 try { 236 return await Resources.readResourcesXMLAsync({ path: filePath }); 237 } catch (error: any) { 238 if (!config.modRequest.introspect) { 239 throw error; 240 } 241 } 242 return { resources: {} }; 243 }, 244 async write(filePath, { modResults, modRequest: { introspect } }) { 245 if (introspect) return; 246 await writeXMLAsync({ path: filePath, xml: modResults }); 247 }, 248 }), 249 250 styles: provider<Resources.ResourceXML>({ 251 isIntrospective: true, 252 253 async getFilePath({ modRequest: { projectRoot, introspect } }) { 254 try { 255 return await Styles.getProjectStylesXMLPathAsync(projectRoot); 256 } catch (error: any) { 257 if (!introspect) { 258 throw error; 259 } 260 } 261 return ''; 262 }, 263 async read(filePath, config) { 264 let styles: Resources.ResourceXML = { resources: {} }; 265 266 try { 267 // Adds support for `tools:x` 268 styles = await Resources.readResourcesXMLAsync({ 269 path: filePath, 270 fallback: `<?xml version="1.0" encoding="utf-8"?><resources xmlns:tools="http://schemas.android.com/tools"></resources>`, 271 }); 272 } catch (error: any) { 273 if (!config.modRequest.introspect) { 274 throw error; 275 } 276 } 277 278 // Ensure support for tools is added... 279 if (!styles.resources.$) { 280 styles.resources.$ = {}; 281 } 282 if (!styles.resources.$?.['xmlns:tools']) { 283 styles.resources.$['xmlns:tools'] = 'http://schemas.android.com/tools'; 284 } 285 return styles; 286 }, 287 async write(filePath, { modResults, modRequest: { introspect } }) { 288 if (introspect) return; 289 await writeXMLAsync({ path: filePath, xml: modResults }); 290 }, 291 }), 292 293 projectBuildGradle: provider<Paths.GradleProjectFile>({ 294 getFilePath({ modRequest: { projectRoot } }) { 295 return Paths.getProjectBuildGradleFilePath(projectRoot); 296 }, 297 async read(filePath) { 298 return Paths.getFileInfo(filePath); 299 }, 300 async write(filePath, { modResults: { contents } }) { 301 await writeFile(filePath, contents); 302 }, 303 }), 304 305 settingsGradle: provider<Paths.GradleProjectFile>({ 306 getFilePath({ modRequest: { projectRoot } }) { 307 return Paths.getSettingsGradleFilePath(projectRoot); 308 }, 309 async read(filePath) { 310 return Paths.getFileInfo(filePath); 311 }, 312 async write(filePath, { modResults: { contents } }) { 313 await writeFile(filePath, contents); 314 }, 315 }), 316 317 appBuildGradle: provider<Paths.GradleProjectFile>({ 318 getFilePath({ modRequest: { projectRoot } }) { 319 return Paths.getAppBuildGradleFilePath(projectRoot); 320 }, 321 async read(filePath) { 322 return Paths.getFileInfo(filePath); 323 }, 324 async write(filePath, { modResults: { contents } }) { 325 await writeFile(filePath, contents); 326 }, 327 }), 328 329 mainActivity: provider<Paths.ApplicationProjectFile>({ 330 getFilePath({ modRequest: { projectRoot } }) { 331 return Paths.getProjectFilePath(projectRoot, 'MainActivity'); 332 }, 333 async read(filePath) { 334 return Paths.getFileInfo(filePath); 335 }, 336 async write(filePath, { modResults: { contents } }) { 337 await writeFile(filePath, contents); 338 }, 339 }), 340 341 mainApplication: provider<Paths.ApplicationProjectFile>({ 342 getFilePath({ modRequest: { projectRoot } }) { 343 return Paths.getProjectFilePath(projectRoot, 'MainApplication'); 344 }, 345 async read(filePath) { 346 return Paths.getFileInfo(filePath); 347 }, 348 async write(filePath, { modResults: { contents } }) { 349 await writeFile(filePath, contents); 350 }, 351 }), 352}; 353 354type AndroidDefaultProviders = typeof defaultProviders; 355 356export function withAndroidBaseMods( 357 config: ExportedConfig, 358 { 359 providers, 360 ...props 361 }: ForwardedBaseModOptions & { providers?: Partial<AndroidDefaultProviders> } = {} 362): ExportedConfig { 363 return withGeneratedBaseMods<AndroidModName>(config, { 364 ...props, 365 platform: 'android', 366 providers: providers ?? getAndroidModFileProviders(), 367 }); 368} 369 370export function getAndroidModFileProviders() { 371 return defaultProviders; 372} 373