1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Bacon
3*8a424bebSJames Ideimport { AndroidManifest, ManifestActivity } from './Manifest';
4082815dcSEvan Baconimport { createAndroidManifestPlugin } from '../plugins/android-plugins';
5082815dcSEvan Baconimport { addWarningAndroid } from '../utils/warnings';
6082815dcSEvan Bacon
7082815dcSEvan Baconexport type IntentFilterProps = {
8082815dcSEvan Bacon  actions: string[];
9082815dcSEvan Bacon  categories: string[];
10082815dcSEvan Bacon  data: {
11082815dcSEvan Bacon    scheme: string;
12082815dcSEvan Bacon    host?: string;
13082815dcSEvan Bacon  }[];
14082815dcSEvan Bacon};
15082815dcSEvan Bacon
16082815dcSEvan Baconexport const withScheme = createAndroidManifestPlugin(setScheme, 'withScheme');
17082815dcSEvan Bacon
18082815dcSEvan Baconexport function getScheme(config: { scheme?: string | string[] }): string[] {
19082815dcSEvan Bacon  if (Array.isArray(config.scheme)) {
20082815dcSEvan Bacon    const validate = (value: any): value is string => typeof value === 'string';
21082815dcSEvan Bacon
22082815dcSEvan Bacon    return config.scheme.filter<string>(validate);
23082815dcSEvan Bacon  } else if (typeof config.scheme === 'string') {
24082815dcSEvan Bacon    return [config.scheme];
25082815dcSEvan Bacon  }
26082815dcSEvan Bacon  return [];
27082815dcSEvan Bacon}
28082815dcSEvan Bacon
29082815dcSEvan Bacon// This plugin used to remove the unused schemes but this is unpredictable because other plugins could add schemes.
30384598e2SBrent Vatne// The only way to reliably remove schemes from the project is to nuke the file and regenerate the code (`npx expo prebuild --clean`).
31082815dcSEvan Bacon// Regardless, having extra schemes isn't a fatal issue and therefore a tolerable compromise is to just add new schemes that aren't currently present.
32082815dcSEvan Baconexport function setScheme(
33082815dcSEvan Bacon  config: Pick<ExpoConfig, 'scheme' | 'android'>,
34082815dcSEvan Bacon  androidManifest: AndroidManifest
35082815dcSEvan Bacon) {
36082815dcSEvan Bacon  const schemes = [
37082815dcSEvan Bacon    ...getScheme(config),
38082815dcSEvan Bacon    // @ts-ignore: TODO: android.scheme is an unreleased -- harder to add to turtle v1.
39082815dcSEvan Bacon    ...getScheme(config.android ?? {}),
40082815dcSEvan Bacon  ];
41082815dcSEvan Bacon  // Add the package name to the list of schemes for easier Google auth and parity with Turtle v1.
42082815dcSEvan Bacon  if (config.android?.package) {
43082815dcSEvan Bacon    schemes.push(config.android.package);
44082815dcSEvan Bacon  }
45082815dcSEvan Bacon  if (schemes.length === 0) {
46082815dcSEvan Bacon    return androidManifest;
47082815dcSEvan Bacon  }
48082815dcSEvan Bacon
49082815dcSEvan Bacon  if (!ensureManifestHasValidIntentFilter(androidManifest)) {
50082815dcSEvan Bacon    addWarningAndroid(
51082815dcSEvan Bacon      'scheme',
52082815dcSEvan Bacon      `Cannot add schemes because the provided manifest does not have a valid Activity with \`android:launchMode="singleTask"\``,
53082815dcSEvan Bacon      'https://expo.fyi/setup-android-uri-scheme'
54082815dcSEvan Bacon    );
55082815dcSEvan Bacon    return androidManifest;
56082815dcSEvan Bacon  }
57082815dcSEvan Bacon
58082815dcSEvan Bacon  // Get the current schemes and remove them from the list of schemes to add.
59082815dcSEvan Bacon  const currentSchemes = getSchemesFromManifest(androidManifest);
60082815dcSEvan Bacon  for (const uri of currentSchemes) {
61082815dcSEvan Bacon    const index = schemes.indexOf(uri);
62082815dcSEvan Bacon    if (index > -1) schemes.splice(index, 1);
63082815dcSEvan Bacon  }
64082815dcSEvan Bacon
65082815dcSEvan Bacon  // Now add all of the remaining schemes.
66082815dcSEvan Bacon  for (const uri of schemes) {
67082815dcSEvan Bacon    androidManifest = appendScheme(uri, androidManifest);
68082815dcSEvan Bacon  }
69082815dcSEvan Bacon
70082815dcSEvan Bacon  return androidManifest;
71082815dcSEvan Bacon}
72082815dcSEvan Bacon
73082815dcSEvan Baconfunction isValidRedirectIntentFilter({ actions, categories }: IntentFilterProps): boolean {
74082815dcSEvan Bacon  return (
75082815dcSEvan Bacon    actions.includes('android.intent.action.VIEW') &&
76082815dcSEvan Bacon    !categories.includes('android.intent.category.LAUNCHER')
77082815dcSEvan Bacon  );
78082815dcSEvan Bacon}
79082815dcSEvan Bacon
80082815dcSEvan Baconfunction propertiesFromIntentFilter(intentFilter: any): IntentFilterProps {
81082815dcSEvan Bacon  const actions = intentFilter?.action?.map((data: any) => data?.$?.['android:name']) ?? [];
82082815dcSEvan Bacon  const categories = intentFilter?.category?.map((data: any) => data?.$?.['android:name']) ?? [];
83082815dcSEvan Bacon  const data =
84082815dcSEvan Bacon    intentFilter?.data
85082815dcSEvan Bacon      ?.filter((data: any) => data?.$?.['android:scheme'])
86082815dcSEvan Bacon      ?.map((data: any) => ({
87082815dcSEvan Bacon        scheme: data?.$?.['android:scheme'],
88082815dcSEvan Bacon        host: data?.$?.['android:host'],
89082815dcSEvan Bacon      })) ?? [];
90082815dcSEvan Bacon  return {
91082815dcSEvan Bacon    actions,
92082815dcSEvan Bacon    categories,
93082815dcSEvan Bacon    data,
94082815dcSEvan Bacon  };
95082815dcSEvan Bacon}
96082815dcSEvan Bacon
97082815dcSEvan Baconfunction getSingleTaskIntentFilters(androidManifest: AndroidManifest): any[] {
98082815dcSEvan Bacon  if (!Array.isArray(androidManifest.manifest.application)) return [];
99082815dcSEvan Bacon
100082815dcSEvan Bacon  let outputSchemes: any[] = [];
101082815dcSEvan Bacon  for (const application of androidManifest.manifest.application) {
102082815dcSEvan Bacon    const { activity } = application;
103082815dcSEvan Bacon    // @ts-ignore
104082815dcSEvan Bacon    const activities = Array.isArray(activity) ? activity : [activity];
105082815dcSEvan Bacon    const singleTaskActivities = (activities as ManifestActivity[]).filter(
106082815dcSEvan Bacon      (activity) => activity?.$?.['android:launchMode'] === 'singleTask'
107082815dcSEvan Bacon    );
108082815dcSEvan Bacon    for (const activity of singleTaskActivities) {
109082815dcSEvan Bacon      const intentFilters = activity['intent-filter'];
110082815dcSEvan Bacon      outputSchemes = outputSchemes.concat(intentFilters);
111082815dcSEvan Bacon    }
112082815dcSEvan Bacon  }
113082815dcSEvan Bacon  return outputSchemes;
114082815dcSEvan Bacon}
115082815dcSEvan Bacon
116082815dcSEvan Baconexport function getSchemesFromManifest(
117082815dcSEvan Bacon  androidManifest: AndroidManifest,
118082815dcSEvan Bacon  requestedHost: string | null = null
119082815dcSEvan Bacon): string[] {
120082815dcSEvan Bacon  const outputSchemes: string[] = [];
121082815dcSEvan Bacon
122082815dcSEvan Bacon  const singleTaskIntentFilters = getSingleTaskIntentFilters(androidManifest);
123082815dcSEvan Bacon  for (const intentFilter of singleTaskIntentFilters) {
124082815dcSEvan Bacon    const properties = propertiesFromIntentFilter(intentFilter);
125082815dcSEvan Bacon    if (isValidRedirectIntentFilter(properties) && properties.data) {
126082815dcSEvan Bacon      for (const { scheme, host } of properties.data) {
127082815dcSEvan Bacon        if (requestedHost === null || !host || host === requestedHost) {
128082815dcSEvan Bacon          outputSchemes.push(scheme);
129082815dcSEvan Bacon        }
130082815dcSEvan Bacon      }
131082815dcSEvan Bacon    }
132082815dcSEvan Bacon  }
133082815dcSEvan Bacon
134082815dcSEvan Bacon  return outputSchemes;
135082815dcSEvan Bacon}
136082815dcSEvan Bacon
137082815dcSEvan Baconexport function ensureManifestHasValidIntentFilter(androidManifest: AndroidManifest): boolean {
138082815dcSEvan Bacon  if (!Array.isArray(androidManifest.manifest.application)) {
139082815dcSEvan Bacon    return false;
140082815dcSEvan Bacon  }
141082815dcSEvan Bacon
142082815dcSEvan Bacon  for (const application of androidManifest.manifest.application) {
143082815dcSEvan Bacon    for (const activity of application.activity || []) {
144082815dcSEvan Bacon      if (activity?.$?.['android:launchMode'] === 'singleTask') {
145082815dcSEvan Bacon        for (const intentFilter of activity['intent-filter'] || []) {
146082815dcSEvan Bacon          // Parse valid intent filters...
147082815dcSEvan Bacon          const properties = propertiesFromIntentFilter(intentFilter);
148082815dcSEvan Bacon          if (isValidRedirectIntentFilter(properties)) {
149082815dcSEvan Bacon            return true;
150082815dcSEvan Bacon          }
151082815dcSEvan Bacon        }
152082815dcSEvan Bacon        if (!activity['intent-filter']) {
153082815dcSEvan Bacon          activity['intent-filter'] = [];
154082815dcSEvan Bacon        }
155082815dcSEvan Bacon
156082815dcSEvan Bacon        activity['intent-filter'].push({
157082815dcSEvan Bacon          action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
158082815dcSEvan Bacon          category: [
159082815dcSEvan Bacon            { $: { 'android:name': 'android.intent.category.DEFAULT' } },
160082815dcSEvan Bacon            { $: { 'android:name': 'android.intent.category.BROWSABLE' } },
161082815dcSEvan Bacon          ],
162082815dcSEvan Bacon        });
163082815dcSEvan Bacon        return true;
164082815dcSEvan Bacon      }
165082815dcSEvan Bacon    }
166082815dcSEvan Bacon  }
167082815dcSEvan Bacon  return false;
168082815dcSEvan Bacon}
169082815dcSEvan Bacon
170082815dcSEvan Baconexport function hasScheme(scheme: string, androidManifest: AndroidManifest): boolean {
171082815dcSEvan Bacon  const schemes = getSchemesFromManifest(androidManifest);
172082815dcSEvan Bacon  return schemes.includes(scheme);
173082815dcSEvan Bacon}
174082815dcSEvan Bacon
175082815dcSEvan Baconexport function appendScheme(scheme: string, androidManifest: AndroidManifest): AndroidManifest {
176082815dcSEvan Bacon  if (!Array.isArray(androidManifest.manifest.application)) {
177082815dcSEvan Bacon    return androidManifest;
178082815dcSEvan Bacon  }
179082815dcSEvan Bacon
180082815dcSEvan Bacon  for (const application of androidManifest.manifest.application) {
181082815dcSEvan Bacon    for (const activity of application.activity || []) {
182082815dcSEvan Bacon      if (activity?.$?.['android:launchMode'] === 'singleTask') {
183082815dcSEvan Bacon        for (const intentFilter of activity['intent-filter'] || []) {
184082815dcSEvan Bacon          const properties = propertiesFromIntentFilter(intentFilter);
185082815dcSEvan Bacon          if (isValidRedirectIntentFilter(properties)) {
186082815dcSEvan Bacon            if (!intentFilter.data) intentFilter.data = [];
187082815dcSEvan Bacon            intentFilter.data.push({
188082815dcSEvan Bacon              $: { 'android:scheme': scheme },
189082815dcSEvan Bacon            });
190082815dcSEvan Bacon          }
191082815dcSEvan Bacon        }
192082815dcSEvan Bacon        break;
193082815dcSEvan Bacon      }
194082815dcSEvan Bacon    }
195082815dcSEvan Bacon  }
196082815dcSEvan Bacon  return androidManifest;
197082815dcSEvan Bacon}
198082815dcSEvan Bacon
199082815dcSEvan Baconexport function removeScheme(scheme: string, androidManifest: AndroidManifest): AndroidManifest {
200082815dcSEvan Bacon  if (!Array.isArray(androidManifest.manifest.application)) {
201082815dcSEvan Bacon    return androidManifest;
202082815dcSEvan Bacon  }
203082815dcSEvan Bacon
204082815dcSEvan Bacon  for (const application of androidManifest.manifest.application) {
205082815dcSEvan Bacon    for (const activity of application.activity || []) {
206082815dcSEvan Bacon      if (activity?.$?.['android:launchMode'] === 'singleTask') {
207082815dcSEvan Bacon        for (const intentFilter of activity['intent-filter'] || []) {
208082815dcSEvan Bacon          // Parse valid intent filters...
209082815dcSEvan Bacon          const properties = propertiesFromIntentFilter(intentFilter);
210082815dcSEvan Bacon          if (isValidRedirectIntentFilter(properties)) {
211082815dcSEvan Bacon            for (const dataKey in intentFilter?.data || []) {
212082815dcSEvan Bacon              const data = intentFilter.data?.[dataKey];
213082815dcSEvan Bacon              if (data?.$?.['android:scheme'] === scheme) {
214082815dcSEvan Bacon                delete intentFilter.data?.[dataKey];
215082815dcSEvan Bacon              }
216082815dcSEvan Bacon            }
217082815dcSEvan Bacon          }
218082815dcSEvan Bacon        }
219082815dcSEvan Bacon        break;
220082815dcSEvan Bacon      }
221082815dcSEvan Bacon    }
222082815dcSEvan Bacon  }
223082815dcSEvan Bacon
224082815dcSEvan Bacon  return androidManifest;
225082815dcSEvan Bacon}
226