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