Skip to content

Commit

Permalink
fix(android): handle resource images/drawables and splash screens
Browse files Browse the repository at this point in the history
- don't mangle .9 suffix in drawables
- 9-patch images must be png, enforce directories where splashes can live
- mangle drawable subdirs into destination filename
  • Loading branch information
sgtcoolguy committed Mar 9, 2021
1 parent 8932ce5 commit 3cd22eb
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 20 deletions.
41 changes: 39 additions & 2 deletions android/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @module cli/_build
*
* @copyright
* Copyright (c) 2009-2020 by Axway, Inc. All Rights Reserved.
* Copyright (c) 2009-2021 by Axway, Inc. All Rights Reserved.
*
* @license
* Licensed under the terms of the Apache Public License
Expand All @@ -22,6 +22,8 @@ const ADB = require('node-titanium-sdk/lib/adb'),
Builder = require('node-titanium-sdk/lib/builder'),
GradleWrapper = require('../lib/gradle-wrapper'),
ProcessJsTask = require('../../../cli/lib/tasks/process-js-task'),
ProcessDrawablesTask = require('../lib/process-drawables-task'),
ProcessSplashesTask = require('../lib/process-splashes-task'),
Color = require('../../../common/lib/color'),
ProcessCSSTask = require('../../../cli/lib/tasks/process-css-task'),
CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task'),
Expand Down Expand Up @@ -2483,7 +2485,8 @@ AndroidBuilder.prototype.gatherResources = async function gatherResources() {
// now categorize (i.e. lump into buckets of js/css/html/assets/generic resources)
const categorizer = new gather.Categorizer({
tiappIcon: this.tiapp.icon,
jsFilesNotToProcess: Object.keys(this.htmlJsFiles)
jsFilesNotToProcess: Object.keys(this.htmlJsFiles),
platform: 'android',
});
return await categorizer.run(combined);
};
Expand All @@ -2504,6 +2507,38 @@ AndroidBuilder.prototype.copyCSSFiles = async function copyCSSFiles(files) {
return task.run();
};

/**
* Copies drawable resources into the app
* @param {Map<string,object>} files map from filename to file info
* @returns {Promise<void>}
*/
AndroidBuilder.prototype.processDrawableFiles = async function processDrawableFiles(files) {
this.logger.debug(__('Copying Drawables'));
const task = new ProcessDrawablesTask({
files,
incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-drawables'),
logger: this.logger,
builder: this,
});
return task.run();
};

/**
* Copies splash screen resources into the app
* @param {Map<string,object>} files map from filename to file info
* @returns {Promise<void>}
*/
AndroidBuilder.prototype.processSplashesFiles = async function processSplashesFiles(files) {
this.logger.debug(__('Copying Splash Screens'));
const task = new ProcessSplashesTask({
files,
incrementalDirectory: path.join(this.buildTiIncrementalDir, 'process-splashes'),
logger: this.logger,
builder: this,
});
return task.run();
};

/**
* Used to de4termine the destination path for special assets (_app_props_.json, bootstrap.json) based on encyption or not.
* @returns {string} destination directory to place file
Expand Down Expand Up @@ -2679,6 +2714,8 @@ AndroidBuilder.prototype.copyResources = async function copyResources() {
await Promise.all([
this.copyCSSFiles(gatheredResults.cssFiles),
this.processJSFiles(gatheredResults.jsFiles),
this.processDrawableFiles(gatheredResults.imageAssets),
this.processSplashesFiles(gatheredResults.launchImages),
this.writeAppProps(), // writes _app_props_.json for Ti.Properties
this.writeEnvironmentVariables(), // writes _env_.json for process.env
this.copyPlatformDirs(), // copies platform/android dirs from project/modules
Expand Down
71 changes: 71 additions & 0 deletions android/cli/lib/process-drawables-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const path = require('path');
const CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task');

const appc = require('node-appc');
const i18n = appc.i18n(__dirname);
const __ = i18n.__;

const drawableDpiRegExp = /^(high|medium|low)$/;

/**
* Task that copies Android drawables into the app.
*/
class ProcessDrawablesTask extends CopyResourcesTask {

/**
* Constructs a new processing task.
*
* @param {Object} options Configuration object for this task.
* @param {String} [options.name='process-drawables'] Name for the task.
* @param {String} options.incrementalDirectory Path to a folder where incremental task data will be stored.
* @param {Map<string,FileInfo>} options.files Map of input files to file info
* @param {Object} [options.logger] The logger instance used by this task.
* @param {Object} options.builder Builder instance.
*/
constructor(options) {
options.name = options.name || 'process-drawables';
const builder = options.builder;
const logger = builder.logger;
const appMainResDir = builder.buildAppMainResDir;

// We have to modify the destination paths for each file. Is there a place we can move this to other than constructor?
// Because we want the expected final filename for whenever the incremental task stuff needs it
options.files.forEach((info, relPath) => {
const parts = relPath.split(path.sep);
const origFoldername = parts[1];
const foldername = drawableDpiRegExp.test(origFoldername) ? 'drawable-' + origFoldername[0] + 'dpi' : 'drawable-' + origFoldername.substring(4);

let base = info.name;
// We have a drawable image file. (Rename it if it contains invalid characters.)
const warningMessages = [];
if (parts.length > 3) {
warningMessages.push(__('- Files cannot be put into subdirectories.'));
// retain subdirs under the res-<dpi> folder to be mangled into the destination filename
// i.e. take images/res-mdpi/logos/app.png and store logos/app, which below will become logos_app.png
base = parts.slice(2, parts.length - 1).join(path.sep) + path.sep + base;
}
const destFilename = `${base}.${info.ext}`;
// basename may have .9 suffix, if so, we do not want to convert that .9 to _9
let destFilteredFilename = `${base.toLowerCase().replace(/(?!\.9$)[^a-z0-9_]/g, '_')}.${info.ext}`;
if (destFilteredFilename !== destFilename) {
warningMessages.push(__('- Names must contain only lowercase a-z, 0-9, or underscore.'));
}
if (/^\d/.test(destFilteredFilename)) {
warningMessages.push(__('- Names cannot start with a number.'));
destFilteredFilename = `_${destFilteredFilename}`;
}
if (warningMessages.length > 0) {
// relPath here is relative the the folder we searched, NOT the project dir, so make full path relative to project dir for log
logger.warn(__(`Invalid "res" file: ${path.relative(builder.projectDir, info.src)}`));
for (const nextMessage of warningMessages) {
logger.warn(nextMessage);
}
logger.warn(__(`- Titanium will rename to: ${destFilteredFilename}`));
}
info.dest = path.join(appMainResDir, foldername, destFilteredFilename);
});
super(options);
}
}

module.exports = ProcessDrawablesTask;
47 changes: 47 additions & 0 deletions android/cli/lib/process-splashes-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const path = require('path');
const CopyResourcesTask = require('../../../cli/lib/tasks/copy-resources-task');

const drawableDpiRegExp = /^(high|medium|low)$/;

/**
* Task that copies Android splash screens into the app.
*/
class ProcessSplashesTask extends CopyResourcesTask {

/**
* Constructs a new processing task.
*
* @param {Object} options Configuration object for this task.
* @param {String} [options.name='process-splashes'] Name for the task.
* @param {String} options.incrementalDirectory Path to a folder where incremental task data will be stored.
* @param {Map<string,FileInfo>} options.files Map of input files to file info
* @param {Object} [options.logger] The logger instance used by this task.
* @param {Object} options.builder Builder instance.
*/
constructor(options) {
options.name = options.name || 'process-splashes';
const appMainResDir = options.builder.buildAppMainResDir;

options.files.forEach((info, relPath) => {
let destDir;
const parts = relPath.split(path.sep);
if (parts.length >= 3) {
// resolution specific splash goes in res/drawable-<res>
const origFoldername = parts[1];
const foldername = drawableDpiRegExp.test(origFoldername) ? 'drawable-' + origFoldername[0] + 'dpi' : 'drawable-' + origFoldername.substring(4);
destDir = path.join(appMainResDir, foldername);
// assume not under images/<res>, but instead root?
} else if (info.name === 'default.9') {
// 9-patch splash screen goes in res/drawable-nodpi
destDir = path.join(appMainResDir, 'drawable-nodpi');
} else { // root splash goes in res/drawable
destDir = path.join(appMainResDir, 'drawable');
}

info.dest = path.join(destDir, `${info.name.replace('default', 'background')}.${info.ext}`);
});
super(options);
}
}

module.exports = ProcessSplashesTask;
58 changes: 40 additions & 18 deletions cli/lib/gather.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const FILENAME_REGEXP = /^(.*)\.(\w+)$/;
const LAUNCH_IMAGE_REGEXP = /^(Default(-(Landscape|Portrait))?(-[0-9]+h)?(@[2-9]x)?)\.png$/;
const LAUNCH_LOGO_REGEXP = /^LaunchLogo(?:@([23])x)?(?:~(iphone|ipad))?\.(?:png|jpg)$/;
const BUNDLE_FILE_REGEXP = /.+\.bundle\/.+/;
// Android-specific stuff
const DRAWABLE_REGEXP = /^images\/(high|medium|low|res-[^/]+)(\/(.*))$/;
const ANDROID_SPLASH_REGEXP = /^(images\/(high|medium|low|res-[^/]+)\/)?default\.(9\.png|png|jpg)$/;

/**
* Merges multiple maps
Expand Down Expand Up @@ -47,9 +50,9 @@ class Result {
this.appIcons = new Map(); // ios specific
this.cssFiles = new Map(); // css files to be processed (minified optionally)
this.jsFiles = new Map(); // js files to be processed (transpiled/sourcemapped/minified/etc)
this.launchImages = new Map(); // ios specific
this.launchImages = new Map(); // Used to create an asset catalog for launch images on iOS, used for splash screen(s) on Android
this.launchLogos = new Map(); // ios specific
this.imageAssets = new Map(); // ios specific
this.imageAssets = new Map(); // used for asset catalogs and app thinning on iOS, used for drawables on Android
this.resourcesToCopy = new Map(); // "plain" files to copy to the app
this.htmlJsFiles = new Set(); // used internally to track js files we shouldn't process (basically move from jsFiles to resourcesToCopy bucket)
}
Expand Down Expand Up @@ -205,9 +208,11 @@ class Categorizer {
* @param {string} options.tiappIcon tiapp icon filename
* @param {string[]} [options.jsFilesNotToProcess=[]] listing of JS files explicitly not to process
* @param {boolean} [options.useAppThinning=false] use app thinning?
* @param {string} [options.platform] 'ios', 'android'
*/
constructor(options) {
this.useAppThinning = options.useAppThinning;
this.platform = options.platform;
this.jsFilesNotToProcess = options.jsFilesNotToProcess || [];

const appIcon = options.tiappIcon.match(FILENAME_REGEXP);
Expand Down Expand Up @@ -251,38 +256,55 @@ class Categorizer {
// FIXME: Only check for these in files in root of the src dir! How can we tell? check against relPath instead of name?
// if (!origSrc) { // I think this is to try and only check in the first root src dir?
if (this.appIconRegExp) {
const m = info.name.match(this.appIconRegExp);
const m = info.name.match(this.appIconRegExp); // FIXME: info.name doesn't include extension right now!
if (m) {
info.tag = m[1];
results.appIcons.set(relPath, info);
return;
}
}

if (LAUNCH_IMAGE_REGEXP.test(info.name)) {
if (this.platform === 'ios' && LAUNCH_IMAGE_REGEXP.test(info.name)) { // FIXME: info.name doesn't include extension right now!
results.launchImages.set(relPath, info);
return;
}
// }
// fall through to lump with JPG...

case 'jpg':
// if the image is the LaunchLogo.png, then let that pass so we can use it
// in the LaunchScreen.storyboard
const m = info.name.match(LAUNCH_LOGO_REGEXP);
if (m) {
info.scale = m[1];
info.device = m[2];
results.launchLogos.set(relPath, info);
if (this.platform === 'android') {
// Toss Android splash screens into launchImages
if (relPath.match(ANDROID_SPLASH_REGEXP)) {
results.launchImages.set(relPath, info);
return;
}
// Toss Android drawables into imageAssets to be processed via ProcessDrawablesTask
if (relPath.match(DRAWABLE_REGEXP)) {
results.imageAssets.set(relPath, info);
return;
}
} else if (this.platform === 'ios') {
// if the image is the LaunchLogo.png, then let that pass so we can use it
// in the LaunchScreen.storyboard
const m = info.name.match(LAUNCH_LOGO_REGEXP); // FIXME: info.name doesn't include extension right now!
if (m) {
info.scale = m[1];
info.device = m[2];
results.launchLogos.set(relPath, info);
return;
}

// if we are using app thinning, then don't copy the image, instead mark the
// image to be injected into the asset catalog. Also, exclude images that are
// managed by their bundles.
} else if (this.useAppThinning && !relPath.match(BUNDLE_FILE_REGEXP)) {
results.imageAssets.set(relPath, info);
} else {
results.resourcesToCopy.set(relPath, info);
// if we are using app thinning, then don't copy the image, instead mark the
// image to be injected into the asset catalog. Also, exclude images that are
// managed by their bundles.
if (this.useAppThinning && !relPath.match(BUNDLE_FILE_REGEXP)) {
results.imageAssets.set(relPath, info);
return;
}
}

// Normal PNG/JPG, so just copy it
results.resourcesToCopy.set(relPath, info);
break;

case 'html':
Expand Down
1 change: 1 addition & 0 deletions iphone/cli/commands/_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -5228,6 +5228,7 @@ iOSBuilder.prototype.gatherResources = async function gatherResources() {
const categorizer = new gather.Categorizer({
tiappIcon: this.tiapp.icon,
useAppThinning: this.useAppThinning,
platform: 'ios',
});
const categorized = await categorizer.run(combined);

Expand Down

0 comments on commit 3cd22eb

Please sign in to comment.