"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AndroidAssetGenerator = void 0; const tslib_1 = require("tslib"); /* eslint-disable @typescript-eslint/no-non-null-assertion */ const utils_fs_1 = require("@ionic/utils-fs"); const path_1 = require("path"); const sharp_1 = (0, tslib_1.__importDefault)(require("sharp")); const asset_generator_1 = require("../../asset-generator"); const error_1 = require("../../error"); const output_asset_1 = require("../../output-asset"); const log_1 = require("../../util/log"); const AndroidAssetTemplates = (0, tslib_1.__importStar)(require("./assets")); class AndroidAssetGenerator extends asset_generator_1.AssetGenerator { constructor(options = {}) { super(options); } async generate(asset, project) { var _a; const androidDir = (_a = project.config.android) === null || _a === void 0 ? void 0 : _a.path; if (!androidDir) { throw new error_1.BadProjectError('No android project found'); } if (asset.platform !== "any" /* Any */ && asset.platform !== "android" /* Android */) { return []; } switch (asset.kind) { case "logo" /* Logo */: case "logo-dark" /* LogoDark */: return this.generateFromLogo(asset, project); case "icon" /* Icon */: return this.generateLegacyIcon(asset, project); case "icon-foreground" /* IconForeground */: return this.generateAdaptiveIconForeground(asset, project); case "icon-background" /* IconBackground */: return this.generateAdaptiveIconBackground(asset, project); case "splash" /* Splash */: case "splash-dark" /* SplashDark */: return this.generateSplashes(asset, project); } return []; } /** * Generate from logo combines all of the other operations into a single operation * from a single asset source file. In this mode, a logo along with a background color * is used to generate all icons and splash screens (with dark mode where possible). */ async generateFromLogo(asset, project) { const pipe = asset.pipeline(); const generated = []; if (!pipe) { throw new error_1.BadPipelineError('Sharp instance not created'); } // Generate adaptive icons const generatedAdaptiveIcons = await this._generateAdaptiveIconsFromLogo(project, asset, pipe); generated.push(...generatedAdaptiveIcons); if (asset.kind === "logo" /* Logo */) { // Generate legacy icons const generatedLegacyIcons = await this.generateLegacyIcon(asset, project); generated.push(...generatedLegacyIcons); const splashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash" /* Splash */); const generatedSplashes = await Promise.all(splashes.map(async (splash) => { var _a; return this._generateSplashesFromLogo(project, asset, splash, pipe, (_a = this.options.splashBackgroundColor) !== null && _a !== void 0 ? _a : '#ffffff'); })); generated.push(...generatedSplashes); } // Generate dark splashes const darkSplashes = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash-dark" /* SplashDark */); const generatedSplashes = await Promise.all(darkSplashes.map(async (splash) => { var _a; return this._generateSplashesFromLogo(project, asset, splash, pipe, (_a = this.options.splashBackgroundColorDark) !== null && _a !== void 0 ? _a : '#111111'); })); generated.push(...generatedSplashes); return [...generated]; } // Generate adaptive icons from the source logo async _generateAdaptiveIconsFromLogo(project, asset, pipe) { var _a, _b; // Current versions of Android don't appear to support night mode icons (13+ might?) // so, for now, we only generate light mode ones if (asset.kind === "logo-dark" /* LogoDark */) { return []; } // Create the background pipeline for the generated icons const backgroundPipe = (0, sharp_1.default)({ create: { width: asset.width, height: asset.height, channels: 4, background: asset.kind === "logo" /* Logo */ ? (_a = this.options.iconBackgroundColor) !== null && _a !== void 0 ? _a : '#ffffff' : (_b = this.options.iconBackgroundColorDark) !== null && _b !== void 0 ? _b : '#111111', }, }); const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "adaptive-icon" /* AdaptiveIcon */); const backgroundImages = await Promise.all(icons.map(async (icon) => { return await this._generateAdaptiveIconBackground(project, asset, icon, backgroundPipe); })); const foregroundImages = await Promise.all(icons.map(async (icon) => { return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); })); return [...foregroundImages, ...backgroundImages]; } async _generateSplashesFromLogo(project, asset, splash, pipe, backgroundColor) { var _a, _b, _c, _d, _e, _f, _g; // Generate light splash const resPath = this.getResPath(project); let drawableDir = `drawable`; if (splash.density) { drawableDir = `drawable-${splash.density}`; } const parentDir = (0, path_1.join)(resPath, drawableDir); if (!(await (0, utils_fs_1.pathExists)(parentDir))) { await (0, utils_fs_1.mkdirp)(parentDir); } const dest = (0, path_1.join)(resPath, drawableDir, 'splash.png'); const targetLogoWidthPercent = (_a = this.options.logoSplashScale) !== null && _a !== void 0 ? _a : 0.2; let targetWidth = (_b = this.options.logoSplashTargetWidth) !== null && _b !== void 0 ? _b : Math.floor(((_c = splash.width) !== null && _c !== void 0 ? _c : 0) * targetLogoWidthPercent); if (targetWidth > splash.width || targetWidth > splash.height) { targetWidth = Math.floor(((_d = splash.width) !== null && _d !== void 0 ? _d : 0) * targetLogoWidthPercent); } if (targetWidth > splash.width || targetWidth > splash.height) { (0, log_1.warn)(`Logo dimensions exceed dimensions of splash ${splash.width}x${splash.height}, using default logo size`); targetWidth = Math.floor(((_e = splash.width) !== null && _e !== void 0 ? _e : 0) * 0.2); } const canvas = (0, sharp_1.default)({ create: { width: (_f = splash.width) !== null && _f !== void 0 ? _f : 0, height: (_g = splash.height) !== null && _g !== void 0 ? _g : 0, channels: 4, background: backgroundColor, }, }); const resized = await (0, sharp_1.default)(asset.path).resize(targetWidth).toBuffer(); const outputInfo = await canvas .composite([{ input: resized, gravity: sharp_1.default.gravity.center }]) .png() .toFile(dest); const splashOutput = new output_asset_1.OutputAsset(splash, asset, project, { [dest]: dest, }, { [dest]: outputInfo, }); return splashOutput; } async generateLegacyIcon(asset, project) { const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); const pipe = asset.pipeline(); if (!pipe) { throw new error_1.BadPipelineError('Sharp instance not created'); } const collected = await Promise.all(icons.map(async (icon) => { const [dest, outputInfo] = await this.generateLegacyLauncherIcon(project, asset, icon); return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher.png`]: dest }, { [`mipmap-${icon.density}/ic_launcher.png`]: outputInfo }); })); collected.push(...(await Promise.all(icons.map(async (icon) => { const [dest, outputInfo] = await this.generateRoundLauncherIcon(project, asset, icon); return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher_round.png`]: dest }, { [`mipmap-${icon.density}/ic_launcher_round.png`]: outputInfo }); })))); await this.updateManifest(project); return collected; } async generateLegacyLauncherIcon(project, asset, template) { const resPath = this.getResPath(project); const parentDir = (0, path_1.join)(resPath, `mipmap-${template.density}`); if (!(await (0, utils_fs_1.pathExists)(parentDir))) { await (0, utils_fs_1.mkdirp)(parentDir); } const destRound = (0, path_1.join)(resPath, `mipmap-${template.density}`, 'ic_launcher.png'); // This pipeline is trick, but we need two separate pipelines // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 const padding = 8; const resized = await (0, sharp_1.default)(asset.path) .resize(template.width, template.height) // .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) .toBuffer(); const composited = await (0, sharp_1.default)(resized) .resize(Math.max(0, template.width - padding * 2), Math.max(0, template.height - padding * 2)) .extend({ top: padding, bottom: padding, left: padding, right: padding, background: { r: 0, g: 0, b: 0, alpha: 0 }, }) .toBuffer(); const outputInfo = await (0, sharp_1.default)(composited).png().toFile(destRound); return [destRound, outputInfo]; } async generateRoundLauncherIcon(project, asset, template) { const svg = ``; const resPath = this.getResPath(project); const destRound = (0, path_1.join)(resPath, `mipmap-${template.density}`, 'ic_launcher_round.png'); // This pipeline is tricky, but we need two separate pipelines // per https://github.com/lovell/sharp/issues/2378#issuecomment-864132578 const resized = await (0, sharp_1.default)(asset.path).resize(template.width, template.height).toBuffer(); const composited = await (0, sharp_1.default)(resized) .composite([{ input: Buffer.from(svg), blend: 'dest-in' }]) .toBuffer(); const outputInfo = await (0, sharp_1.default)(composited).png().toFile(destRound); return [destRound, outputInfo]; } async generateAdaptiveIconForeground(asset, project) { const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); const pipe = asset.pipeline(); if (!pipe) { throw new error_1.BadPipelineError('Sharp instance not created'); } return Promise.all(icons.map(async (icon) => { return await this._generateAdaptiveIconForeground(project, asset, icon, pipe); })); } async _generateAdaptiveIconForeground(project, asset, icon, pipe) { const resPath = this.getResPath(project); // Create the foreground and background images const destForeground = (0, path_1.join)(resPath, `mipmap-${icon.density}`, 'ic_launcher_foreground.png'); const parentDir = (0, path_1.dirname)(destForeground); if (!(await (0, utils_fs_1.pathExists)(parentDir))) { await (0, utils_fs_1.mkdirp)(parentDir); } const outputInfoForeground = await pipe.resize(icon.width, icon.height).png().toFile(destForeground); // Create the adaptive icon XML const icLauncherXml = ` `.trim(); const mipmapAnyPath = (0, path_1.join)(resPath, `mipmap-anydpi-v26`); if (!(await (0, utils_fs_1.pathExists)(mipmapAnyPath))) { await (0, utils_fs_1.mkdirp)(mipmapAnyPath); } const destIcLauncher = (0, path_1.join)(mipmapAnyPath, `ic_launcher.xml`); const destIcLauncherRound = (0, path_1.join)(mipmapAnyPath, `ic_launcher_round.xml`); await (0, utils_fs_1.writeFile)(destIcLauncher, icLauncherXml); await (0, utils_fs_1.writeFile)(destIcLauncherRound, icLauncherXml); // Return the created files for this OutputAsset return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher_foreground.png`]: destForeground, 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, }, { [`mipmap-${icon.density}/ic_launcher_foreground.png`]: outputInfoForeground, }); } async generateAdaptiveIconBackground(asset, project) { const icons = Object.values(AndroidAssetTemplates).filter((a) => a.kind === "icon" /* Icon */); const pipe = asset.pipeline(); if (!pipe) { throw new error_1.BadPipelineError('Sharp instance not created'); } return Promise.all(icons.map(async (icon) => { return await this._generateAdaptiveIconBackground(project, asset, icon, pipe); })); } async _generateAdaptiveIconBackground(project, asset, icon, pipe) { const resPath = this.getResPath(project); const destBackground = (0, path_1.join)(resPath, `mipmap-${icon.density}`, 'ic_launcher_background.png'); const parentDir = (0, path_1.dirname)(destBackground); if (!(await (0, utils_fs_1.pathExists)(parentDir))) { await (0, utils_fs_1.mkdirp)(parentDir); } const outputInfoBackground = await pipe.resize(icon.width, icon.height).png().toFile(destBackground); // Create the adaptive icon XML const icLauncherXml = ` `.trim(); const mipmapAnyPath = (0, path_1.join)(resPath, `mipmap-anydpi-v26`); if (!(await (0, utils_fs_1.pathExists)(mipmapAnyPath))) { await (0, utils_fs_1.mkdirp)(mipmapAnyPath); } const destIcLauncher = (0, path_1.join)(mipmapAnyPath, `ic_launcher.xml`); const destIcLauncherRound = (0, path_1.join)(mipmapAnyPath, `ic_launcher_round.xml`); await (0, utils_fs_1.writeFile)(destIcLauncher, icLauncherXml); await (0, utils_fs_1.writeFile)(destIcLauncherRound, icLauncherXml); // Return the created files for this OutputAsset return new output_asset_1.OutputAsset(icon, asset, project, { [`mipmap-${icon.density}/ic_launcher_background.png`]: destBackground, 'mipmap-anydpi-v26/ic_launcher.xml': destIcLauncher, 'mipmap-anydpi-v26/ic_launcher_round.xml': destIcLauncherRound, }, { [`mipmap-${icon.density}/ic_launcher_background.png`]: outputInfoBackground, }); } async updateManifest(project) { var _a, _b; (_b = (_a = project.android) === null || _a === void 0 ? void 0 : _a.getAndroidManifest()) === null || _b === void 0 ? void 0 : _b.setAttrs('manifest/application', { 'android:icon': '@mipmap/ic_launcher', 'android:roundIcon': '@mipmap/ic_launcher_round', }); await project.commit(); } async generateSplashes(asset, project) { const pipe = asset.pipeline(); if (!pipe) { throw new error_1.BadPipelineError('Sharp instance not created'); } const splashes = (asset.kind === "splash" /* Splash */ ? Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash" /* Splash */) : Object.values(AndroidAssetTemplates).filter((a) => a.kind === "splash-dark" /* SplashDark */)); const resPath = this.getResPath(project); const collected = await Promise.all(splashes.map(async (splash) => { const [dest, outputInfo] = await this.generateSplash(project, asset, splash, pipe); const relPath = (0, path_1.relative)(resPath, dest); return new output_asset_1.OutputAsset(splash, asset, project, { [relPath]: dest }, { [relPath]: outputInfo }); })); return collected; } async generateSplash(project, asset, template, pipe) { const drawableDir = template.density ? `drawable-${template.density}` : 'drawable'; const resPath = this.getResPath(project); const parentDir = (0, path_1.join)(resPath, drawableDir); if (!(await (0, utils_fs_1.pathExists)(parentDir))) { await (0, utils_fs_1.mkdirp)(parentDir); } const dest = (0, path_1.join)(resPath, drawableDir, 'splash.png'); const outputInfo = await pipe.resize(template.width, template.height).png().toFile(dest); return [dest, outputInfo]; } getResPath(project) { var _a; return (0, path_1.join)(project.config.android.path, 'app', 'src', (_a = this.options.androidFlavor) !== null && _a !== void 0 ? _a : 'main', 'res'); } } exports.AndroidAssetGenerator = AndroidAssetGenerator;