1import { AndroidConfig, ConfigPlugin, withDangerousMod } from '@expo/config-plugins';
2import {
3  buildResourceItem,
4  readResourcesXMLAsync,
5} from '@expo/config-plugins/build/android/Resources';
6import { writeXMLAsync } from '@expo/config-plugins/build/android/XML';
7import { createAndroidManifestPlugin } from '@expo/config-plugins/build/plugins/android-plugins';
8import { ExpoConfig } from '@expo/config-types';
9import { generateImageAsync } from '@expo/image-utils';
10import fs from 'fs-extra';
11import path from 'path';
12
13const { Colors } = AndroidConfig;
14const { ANDROID_RES_PATH, dpiValues } = AndroidConfig.Icon;
15const {
16  addMetaDataItemToMainApplication,
17  getMainApplicationOrThrow,
18  removeMetaDataItemFromMainApplication,
19} = AndroidConfig.Manifest;
20const BASELINE_PIXEL_SIZE = 24;
21export const META_DATA_NOTIFICATION_ICON = 'expo.modules.notifications.default_notification_icon';
22export const META_DATA_NOTIFICATION_ICON_COLOR =
23  'expo.modules.notifications.default_notification_color';
24export const NOTIFICATION_ICON = 'notification_icon';
25export const NOTIFICATION_ICON_RESOURCE = `@drawable/${NOTIFICATION_ICON}`;
26export const NOTIFICATION_ICON_COLOR = 'notification_icon_color';
27export const NOTIFICATION_ICON_COLOR_RESOURCE = `@color/${NOTIFICATION_ICON_COLOR}`;
28
29export const withNotificationIcons: ConfigPlugin = config => {
30  return withDangerousMod(config, [
31    'android',
32    async config => {
33      await setNotificationIconAsync(config, config.modRequest.projectRoot);
34      return config;
35    },
36  ]);
37};
38
39export const withNotificationIconColor: ConfigPlugin = config => {
40  return withDangerousMod(config, [
41    'android',
42    async config => {
43      await setNotificationIconColorAsync(config, config.modRequest.projectRoot);
44      return config;
45    },
46  ]);
47};
48
49export const withNotificationManifest = createAndroidManifestPlugin(
50  setNotificationConfigAsync,
51  'withNotificationManifest'
52);
53
54export function getNotificationIcon(config: ExpoConfig) {
55  return config.notification?.icon || null;
56}
57
58export function getNotificationColor(config: ExpoConfig) {
59  return config.notification?.color || null;
60}
61
62/**
63 * Applies configuration for expo-notifications, including
64 * the notification icon and notification color.
65 */
66export async function setNotificationIconAsync(config: ExpoConfig, projectRoot: string) {
67  const icon = getNotificationIcon(config);
68  if (icon) {
69    await writeNotificationIconImageFilesAsync(icon, projectRoot);
70  } else {
71    await removeNotificationIconImageFilesAsync(projectRoot);
72  }
73}
74
75export async function setNotificationConfigAsync(
76  config: ExpoConfig,
77  manifest: AndroidConfig.Manifest.AndroidManifest
78) {
79  const icon = getNotificationIcon(config);
80  const color = getNotificationColor(config);
81  const mainApplication = getMainApplicationOrThrow(manifest);
82  if (icon) {
83    addMetaDataItemToMainApplication(
84      mainApplication,
85      META_DATA_NOTIFICATION_ICON,
86      NOTIFICATION_ICON_RESOURCE,
87      'resource'
88    );
89  } else {
90    removeMetaDataItemFromMainApplication(mainApplication, META_DATA_NOTIFICATION_ICON);
91  }
92  if (color) {
93    addMetaDataItemToMainApplication(
94      mainApplication,
95      META_DATA_NOTIFICATION_ICON_COLOR,
96      NOTIFICATION_ICON_COLOR_RESOURCE,
97      'resource'
98    );
99  } else {
100    removeMetaDataItemFromMainApplication(mainApplication, META_DATA_NOTIFICATION_ICON_COLOR);
101  }
102  return manifest;
103}
104
105export async function setNotificationIconColorAsync(config: ExpoConfig, projectRoot: string) {
106  const color = getNotificationColor(config);
107  const colorsXmlPath = await Colors.getProjectColorsXMLPathAsync(projectRoot);
108  let colorsJson = await readResourcesXMLAsync({ path: colorsXmlPath });
109  if (color) {
110    const colorItemToAdd = buildResourceItem({ name: NOTIFICATION_ICON_COLOR, value: color });
111    colorsJson = Colors.setColorItem(colorItemToAdd, colorsJson);
112  } else {
113    colorsJson = Colors.removeColorItem(NOTIFICATION_ICON_COLOR, colorsJson);
114  }
115  await writeXMLAsync({ path: colorsXmlPath, xml: colorsJson });
116}
117
118async function writeNotificationIconImageFilesAsync(icon: string, projectRoot: string) {
119  await Promise.all(
120    Object.values(dpiValues).map(async ({ folderName, scale }) => {
121      const drawableFolderName = folderName.replace('mipmap', 'drawable');
122      const dpiFolderPath = path.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName);
123      await fs.ensureDir(dpiFolderPath);
124      const iconSizePx = BASELINE_PIXEL_SIZE * scale;
125
126      try {
127        const resizedIcon = (
128          await generateImageAsync(
129            { projectRoot, cacheType: 'android-notification' },
130            {
131              src: icon,
132              width: iconSizePx,
133              height: iconSizePx,
134              resizeMode: 'cover',
135              backgroundColor: 'transparent',
136            }
137          )
138        ).source;
139        await fs.writeFile(path.resolve(dpiFolderPath, NOTIFICATION_ICON + '.png'), resizedIcon);
140      } catch (e) {
141        throw new Error('Encountered an issue resizing Android notification icon: ' + e);
142      }
143    })
144  );
145}
146
147async function removeNotificationIconImageFilesAsync(projectRoot: string) {
148  await Promise.all(
149    Object.values(dpiValues).map(async ({ folderName }) => {
150      const drawableFolderName = folderName.replace('mipmap', 'drawable');
151      const dpiFolderPath = path.resolve(projectRoot, ANDROID_RES_PATH, drawableFolderName);
152      await fs.remove(path.resolve(dpiFolderPath, NOTIFICATION_ICON + '.png'));
153    })
154  );
155}
156
157export const withNotificationsAndroid: ConfigPlugin = config => {
158  config = withNotificationIconColor(config);
159  config = withNotificationIcons(config);
160  config = withNotificationManifest(config);
161  return config;
162};
163