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