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