1import { ConfigPlugin, withDangerousMod } from '@expo/config-plugins';
2import { ExpoConfig } from '@expo/config-types';
3import { generateImageAsync } from '@expo/image-utils';
4import fs from 'fs-extra';
5import path from 'path';
6
7import {
8  getAndroidDarkSplashConfig,
9  getAndroidSplashConfig,
10  SplashScreenConfig,
11} from './getAndroidSplashConfig';
12
13type DRAWABLE_SIZE = 'default' | 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi';
14type THEME = 'light' | 'dark';
15
16const IMAGE_CACHE_NAME = 'splash-android';
17const SPLASH_SCREEN_FILENAME = 'splashscreen_image.png';
18const DRAWABLES_CONFIGS: {
19  [key in DRAWABLE_SIZE]: {
20    modes: {
21      [key in THEME]: {
22        path: string;
23      };
24    };
25    dimensionsMultiplier: number;
26  };
27} = {
28  default: {
29    modes: {
30      light: {
31        path: `./res/drawable/${SPLASH_SCREEN_FILENAME}`,
32      },
33      dark: {
34        path: `./res/drawable-night/${SPLASH_SCREEN_FILENAME}`,
35      },
36    },
37    dimensionsMultiplier: 1,
38  },
39  mdpi: {
40    modes: {
41      light: {
42        path: `./res/drawable-mdpi/${SPLASH_SCREEN_FILENAME}`,
43      },
44      dark: {
45        path: `./res/drawable-night-mdpi/${SPLASH_SCREEN_FILENAME}`,
46      },
47    },
48    dimensionsMultiplier: 1,
49  },
50  hdpi: {
51    modes: {
52      light: {
53        path: `./res/drawable-hdpi/${SPLASH_SCREEN_FILENAME}`,
54      },
55      dark: {
56        path: `./res/drawable-night-hdpi/${SPLASH_SCREEN_FILENAME}`,
57      },
58    },
59    dimensionsMultiplier: 1.5,
60  },
61  xhdpi: {
62    modes: {
63      light: {
64        path: `./res/drawable-xhdpi/${SPLASH_SCREEN_FILENAME}`,
65      },
66      dark: {
67        path: `./res/drawable-night-xhdpi/${SPLASH_SCREEN_FILENAME}`,
68      },
69    },
70    dimensionsMultiplier: 2,
71  },
72  xxhdpi: {
73    modes: {
74      light: {
75        path: `./res/drawable-xxhdpi/${SPLASH_SCREEN_FILENAME}`,
76      },
77      dark: {
78        path: `./res/drawable-night-xxhdpi/${SPLASH_SCREEN_FILENAME}`,
79      },
80    },
81    dimensionsMultiplier: 3,
82  },
83  xxxhdpi: {
84    modes: {
85      light: {
86        path: `./res/drawable-xxxhdpi/${SPLASH_SCREEN_FILENAME}`,
87      },
88      dark: {
89        path: `./res/drawable-night-xxxhdpi/${SPLASH_SCREEN_FILENAME}`,
90      },
91    },
92    dimensionsMultiplier: 4,
93  },
94};
95
96export const withAndroidSplashImages: ConfigPlugin = (config) => {
97  return withDangerousMod(config, [
98    'android',
99    async (config) => {
100      await setSplashImageDrawablesAsync(config, config.modRequest.projectRoot);
101      return config;
102    },
103  ]);
104};
105
106/**
107 * Deletes all previous splash_screen_images and copies new one to desired drawable directory.
108 * If path isn't provided then no new image is placed in drawable directories.
109 * @see https://developer.android.com/training/multiscreen/screendensities
110 *
111 * @param androidMainPath Absolute path to the main directory containing code and resources in Android project. In general that would be `android/app/src/main`.
112 */
113export async function setSplashImageDrawablesAsync(
114  config: Pick<ExpoConfig, 'android' | 'splash'>,
115  projectRoot: string
116) {
117  await clearAllExistingSplashImagesAsync(projectRoot);
118
119  const splash = getAndroidSplashConfig(config);
120  const darkSplash = getAndroidDarkSplashConfig(config);
121
122  await Promise.all([
123    setSplashImageDrawablesForThemeAsync(splash, 'light', projectRoot),
124    setSplashImageDrawablesForThemeAsync(darkSplash, 'dark', projectRoot),
125  ]);
126}
127
128async function clearAllExistingSplashImagesAsync(projectRoot: string) {
129  const androidMainPath = path.join(projectRoot, 'android/app/src/main');
130
131  await Promise.all(
132    Object.values(DRAWABLES_CONFIGS).map(async ({ modes }) => {
133      await Promise.all(
134        Object.values(modes).map(async ({ path: filePath }) => {
135          if (await fs.pathExists(path.resolve(androidMainPath, filePath))) {
136            await fs.remove(path.resolve(androidMainPath, filePath));
137          }
138        })
139      );
140    })
141  );
142}
143
144export async function setSplashImageDrawablesForThemeAsync(
145  config: SplashScreenConfig | null,
146  theme: 'dark' | 'light',
147  projectRoot: string
148) {
149  if (!config) return;
150  const androidMainPath = path.join(projectRoot, 'android/app/src/main');
151
152  await Promise.all(
153    ['mdpi', 'hdpi', 'xhdpi', 'xxhdpi', 'xxxhdpi'].map(async (imageKey) => {
154      // @ts-ignore
155      const image = config[imageKey];
156      if (image) {
157        // Using this method will cache the images in `.expo` based on the properties used to generate them.
158        // this method also supports remote URLs and using the global sharp instance.
159        const { source } = await generateImageAsync({ projectRoot, cacheType: IMAGE_CACHE_NAME }, {
160          src: image,
161        } as any);
162
163        // Get output path for drawable.
164        const outputPath = path.join(
165          androidMainPath,
166          // @ts-ignore
167          DRAWABLES_CONFIGS[imageKey].modes[theme].path
168        );
169        // Ensure directory exists.
170        const folder = path.dirname(outputPath);
171        await fs.ensureDir(folder);
172        // Write image buffer to the file system.
173        await fs.writeFile(outputPath, source);
174      }
175      return null;
176    })
177  );
178}
179