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