From 105c380aed22c394402b317ca5801e0ad71f4475 Mon Sep 17 00:00:00 2001 From: larpon <768942+larpon@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:36:25 +0200 Subject: [PATCH] package: support generating icon `mipmap-(xxx)(h/m)dpi` entries via `--icon-mipmaps` (#328) --- README.md | 2 +- android/package.v | 121 ++++++++++++++++++++++++++++++++++++++-------- cli/cli.v | 1 + cli/options.v | 2 + docs/CHANGELOG.md | 21 ++++++++ docs/FAQ.md | 19 ++++++++ docs/docs.md | 53 ++++++++++++-------- 7 files changed, 178 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 1931c2b3..6cf558ac 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ You can build an Android app ready for the Play Store with the following command ```bash export KEYSTORE_PASSWORD="pass" export KEYSTORE_ALIAS_PASSWORD="word" -vab -prod --name "V App" --package-id "com.example.app.id" --icon /path/to/file.png --version-code --keystore /path/to/sign.keystore --keystore-alias "example" /path/to/v/source/file/or/dir +vab -prod --name "V App" --package-id "com.example.app.id" --icon-mipmaps --icon /path/to/file.png --version-code --keystore /path/to/sign.keystore --keystore-alias "example" /path/to/v/source/file/or/dir ``` Do not submit apps using default values. Please make sure to adhere to all [guidelines](https://developer.android.com/studio/publish) of the app store you're publishing to. diff --git a/android/package.v b/android/package.v index 56236f39..005031ae 100644 --- a/android/package.v +++ b/android/package.v @@ -3,6 +3,7 @@ module android import os +import stbi import regex import semver import vab.java @@ -20,6 +21,7 @@ pub const default_min_sdk_version = int($d('vab:default_min_sdk_version', 21)) pub const default_base_files_path = get_default_base_files_path() pub const supported_package_formats = ['apk', 'aab'] pub const supported_lib_folders = ['armeabi', 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'] +pub const mipmap_icon_sizes = [192, 144, 96, 72, 48]! // xxxhdpi, xxhdpi, xhdpi, hdpi, mdpi // PackageFormat holds all supported package formats pub enum PackageFormat { @@ -43,6 +45,7 @@ pub: package_id string activity_name string icon string + icon_mipmaps bool version_code int v_flags []string input string @@ -65,6 +68,13 @@ pub fn (po &PackageOptions) verbose(verbosity_level int, msg string) { } } +// package_root returns the path to the "package base files" that `vab` +// (and the Java/SDK packaging tools) uses as a base for what to include in +// the resulting APK or AAB package file archive. +pub fn (po &PackageOptions) package_root() string { + return os.join_path(po.work_dir, 'package', '${po.format}') +} + fn get_default_base_files_path() string { user_default_base_files_path := $d('vab:default_base_files_path', '') if user_default_base_files_path != '' { @@ -927,9 +937,11 @@ pub: assets_path string // Path to assets } -// prepare_package_base prepares and modifies a package skeleton and returns the paths to them. -// A "package skeleton" is a special structure of directories and files that `vab`'s -// packaging step use to make the final APK or AAB package. +// prepare_package_base prepares, modifies a "package base files" / app skeleton +// and returns useful paths to itself and paths within it. +// +// An "Package base files" / App skeleton" is a special structure of files and +// directories that `vab`'s packaging step use as a basis to make the final APK or AAB package. // prepare_package_base is run before Java tooling does the actual packaging. // // Preparing includes operations such as: @@ -937,17 +949,9 @@ pub: // * Modifying template files, like `AndroidManifest.xml` or the Java Activity // * Moving files into place // * Copy assets to a location where `vab` can pick them up -fn prepare_package_base(opt PackageOptions) !PackageBase { - format := match opt.format { - .apk { - 'apk' - } - .aab { - 'aab' - } - } - opt.verbose(1, 'Preparing ${format} base"') - package_path := os.join_path(opt.work_dir, 'package', format) +pub fn prepare_package_base(opt PackageOptions) !PackageBase { + opt.verbose(1, 'Preparing ${opt.format} base"') + package_path := opt.package_root() opt.verbose(2, 'Removing previous package directory "${package_path}"') os.rmdir_all(package_path) or {} paths.ensure(package_path) or { return error('${@FN}: ${err}') } @@ -1215,16 +1219,39 @@ fn prepare_package_base(opt PackageOptions) !PackageBase { os.write_file(strings_path, content) or { return error('${@FN}: ${err}') } } + return PackageBase{ + package_path: package_path + assets_path: prepare_assets(opt)! + } +} + +// prepare_assets depends on prepare_package_base... +fn prepare_assets(opt PackageOptions) !string { + package_path := opt.package_root() + opt.verbose(1, 'Copying assets...') + icon_path := os.join_path(package_path, 'res', 'mipmap') is_default_pkg_id := opt.package_id == opt.default_package_id if !is_default_pkg_id && os.is_file(opt.icon) && os.file_ext(opt.icon) == '.png' { - icon_path := os.join_path(package_path, 'res', 'mipmap') - paths.ensure(icon_path) or { panic(err) } + paths.ensure(icon_path) or { return error('${@FN}: ${err}') } icon_file := os.join_path(icon_path, 'icon.png') opt.verbose(1, 'Copying icon...') os.rm(icon_file) or {} - os.cp(opt.icon, icon_file) or { panic(err) } + os.cp(opt.icon, icon_file) or { return error('${@FN}: ${err}') } + } + if opt.icon_mipmaps { + out_path := os.dir(icon_path) // should be "res" directory + template_icon_file := if opt.icon != '' { + opt.icon + } else { + ls := os.walk_ext(icon_path, '.png') + ls[ls.index(ls[0] or { '' })] or { '' } + } + if os.is_file(template_icon_file) { + opt.verbose(1, 'Generating mipmap icons...') + make_icon_mipmaps(template_icon_file, out_path) + } } assets_path := os.join_path(package_path, 'assets') @@ -1295,9 +1322,63 @@ fn prepare_package_base(opt PackageOptions) !PackageBase { } } } - return PackageBase{ - package_path: package_path - assets_path: assets_path + return assets_path +} + +fn make_icon_mipmaps(icon_file string, out_path string) { + mut img := stbi.load(icon_file, desired_channels: 0) or { + vabutil.vab_error('${@FN}: error loading ${icon_file}: ${err}') + return + } + defer { img.free() } + + mut threads := []thread{} + for size in mipmap_icon_sizes { + threads << spawn make_icon_mipmap(img, out_path, size, size) + } + threads.wait() +} + +fn make_icon_mipmap(img stbi.Image, out_path string, w int, h int) { + res_str := match w { + 192 { + 'xxxhdpi' + } + 144 { + 'xxhdpi' + } + 96 { + 'xhdpi' + } + 72 { + 'hdpi' + } + 48 { + 'mdpi' + } + else { + vabutil.vab_error('${@FN}: unsupported width of ${w} passed') + return + } + } + rs_img := stbi.resize_uint8(img, w, h) or { + vabutil.vab_error('${@FN}: error resizing ${w} to ${out_path}: ${err}') + return + } + defer { rs_img.free() } + new_path := os.join_path(out_path, 'mipmap-${res_str}') + + // eprintln(rs_img) + os.mkdir_all(new_path) or { + vabutil.vab_error('${@FN}: error creating output directory ${new_path}: ${err}') + return + } + out_file := os.join_path(new_path, 'icon.png') + os.rm(out_file) or {} + stbi.stbi_write_png(out_file, rs_img.width, rs_img.height, rs_img.nr_channels, rs_img.data, + (rs_img.width * rs_img.nr_channels)) or { + vabutil.vab_error('${@FN}: error writing output file ${out_file}: ${err}') + return } } diff --git a/cli/cli.v b/cli/cli.v index a930d2e6..1ab59c89 100644 --- a/cli/cli.v +++ b/cli/cli.v @@ -188,6 +188,7 @@ pub fn args_to_options(arguments []string, defaults Options) !(Options, &flag.Fl activity_name: fp.string('activity-name', 0, defaults.activity_name, 'The name of the main activity (e.g. "VActivity")') icon: fp.string('icon', 0, defaults.icon, 'App icon') + icon_mipmaps: fp.bool('icon-mipmaps', 0, defaults.icon_mipmaps, 'Generate App mipmap(-xxxhdpi etc.) icons from either `--icon` or, if exists, a .png in app skeleton "res/mipmap" directory') version_code: fp.int('version-code', 0, defaults.version_code, 'Build version code (android:versionCode)') // output: fp.string('output', `o`, defaults.output, 'Path to output (dir/file)') diff --git a/cli/options.v b/cli/options.v index cd73307e..c68dd916 100644 --- a/cli/options.v +++ b/cli/options.v @@ -64,6 +64,7 @@ pub mut: c_flags []string @[long: 'cflag'; short: c; xdoc: 'Additional flags for the C compiler'] v_flags []string @[long: 'flag'; short: f; xdoc: 'Additional flags for the V compiler'] lib_name string @[ignore] // Generated field depending on names in input/flags + icon_mipmaps bool @[xdoc: 'Generate App mipmap(-xxxhdpi etc.) icons from either `--icon` or, if exists, a .png in app skeleton "res/mipmap" directory'] assets_extra []string @[long: 'assets'; short: a; xdoc: 'Asset dir(s) to include in build'] libs_extra []string @[long: 'libs'; short: l; xdoc: 'Lib dir(s) to include in build'] version_code int @[xdoc: 'Build version code (android:versionCode)'] @@ -908,6 +909,7 @@ pub fn (opt &Options) as_android_package_options() android.PackageOptions { format: format activity_name: opt.activity_name icon: opt.icon + icon_mipmaps: opt.icon_mipmaps version_code: opt.version_code v_flags: opt.v_flags input: opt.input diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b2d23420..1de079ad 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,24 @@ +## vab next + +#### Notable changes + +Allow for compile-time tweaks of default values: +* `default_app_name` via `-d vab:default_app_name='V Test App'` +* `default_package_id` via `-d vab:default_package_id='io.v.android'` +* `default_activity_name` via `-d vab:default_activity_name='VActivity'` +* `default_package_format` via `-d vab:default_package_format='apk'` +* `default_min_sdk_version` = `-d vab:default_min_sdk_version=21` +* `default_base_files_path` via `-d vab:default_base_files_path=''` + +Add support for generating APK/AAB icon mipmaps. + +##### Example + +```bash +vab --icon-mipmaps --icon ~/v/examples/2048/demo.png ~/v/examples/2048 -o /tmp/2048.apk +unzip -l /tmp/2048.apk # Should list "res/mipmap-xxxhdpi/icon.png" etc. entries +``` + ## vab 0.4.3 *11 October 2024* diff --git a/docs/FAQ.md b/docs/FAQ.md index 220aeaa0..99aaf2b9 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,7 @@ # Freqently Asked Questions - [Where is the `examples` folder?](#where-is-the-examples-folder) +- [Generating `mipmap-xxxhdpi` icons in the APK/AAB](#generating-mipmap-xxxhdpi-icons-in-the-apkaab) - [`vab` can't find my device when deploying?](#vab-cant-find-my-device-when-deploying) - [The app force closes/crashes when I start it?](#the-app-force-closescrashes-when-i-start-it) - [`vab` can't find my SDK/NDK/JAVA_HOME?](#vab-cant-find-my-SDKNDKJAVA_HOME) @@ -25,6 +26,24 @@ Note that not all of V's examples have been written with Android in mind and may thus fail to compile or run properly, pull requests with Android fixes are welcome. +## Generating `mipmap-xxxhdpi` icons in the APK/AAB + +Per default `vab` tries to keep APK/AAB's as "slim" as possible. +So, per default, only one application icon is used/included when building packages. + +If you want more icons for more screen sizes `vab` supports generating these when +packing everything up for distribution via the `--icon-mipmaps` flag. + +When passing `--icon-mipmaps`, the icon mipmaps will be generated based on the +image passed via `--icon /path/to/icon.png`, or if `--icon` is *not* passed (or invalid), +`vab` will try and generate the mipmaps based on what image *may* reside in the +"package base files" "`res/mipmap"` directory. + +For a vanilla build of `vab` the mipmap icons will thus be generated based on: +`platforms/android/res/mipmap/icon.png` + +See [Package base files](https://github.com/vlang/vab/blob/master/docs/docs.md#package-base-files) for more info. + ## `vab` can't find my device when deploying? You [need to enable debugging](https://developer.android.com/studio/command-line/adb#Enabling) on your device. diff --git a/docs/docs.md b/docs/docs.md index a34e0524..616f1588 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -191,35 +191,48 @@ android.deploy(deploy_opt) or { panic(err) } ``` # Package base files -"Package base files" are special directory structures usually found next to the executable -named `platforms/android`. Both `vab` itself and/or any *[extra commands](#extending-vab)* -can have a [`plaforms/android`]() directory in the root of the project the that contains -files that forms the basis of the APK/AAB package being built. The directories -mostly follow the same structure but often provides different entires such as: +"Package base files" (also sometimes referred to as "App skeleton") is a directory +containing files and special directory tree structures that `vab` +(and the Java/SDK packaging tools) use as a base for what to include in +the resulting APK or AAB package file archive when compilation/building is done. -* Custom `AndroidManifest.xml` tailored for the application. -* Custom Java sources for e.g. the "main" activity (under `platforms/android/src`). -* Custom resources like strings, and icons (under `platforms/android/res`). +It is usually found in a project's root next to the *executable* named +"`platforms/android`". -**NOTE** Package base files can also be provided/tweaked by user application sources -via *their* `platforms/android` directory, or via the explicit `--package-overrides` flag, -which will copy all contents of `--package-overrides ` *on top of* the contents -provided as package base files. This allows for tweaking certain code bases instead -of reshipping everything. +Both `vab` itself and/or any *[extra commands](#extending-vab)* can have a [`plaforms/android`](https://github.com/vlang/vab/tree/master/platforms/android) +directory in the root of the project that contains files forming +the basis of the APK/AAB package being built. -Also note that directories named "`java`" in root of projects can act as *implicit* -`--package-overrides`... While this is not ideal, it has historically been a very useful -way for modules to provide tweaks to `vab`'s default package base files. +The directories mostly follow the same structure and often provides different entires such as: -A similar approach (a special `jni` directory) is [being used](https://github.com/libsdl-org/SDL/tree/main/android-project/app/jni) -by the Android NDKs own tooling (`ndk-build`) for various reasons and can thus be -found in other projects where it serves similar inclusion purposes. -`vab` does not treat any `jni` directories specially. +* Custom `AndroidManifest.xml` tailored for the application/project. +* Custom Java sources for e.g. the "main" Java activity (under `platforms/android/src`). +* Custom resources like strings and icons (under `platforms/android/res`). See also [`fn prepare_package_base(opt PackageOptions) !PackageBase`](https://github.com/vlang/vab/blob/86d23cd703c0cfc2ce7df82535369a98d2f9d3b0/android/package.v#L940) in `android/package.v` as well as [`--icon-mipmaps`](https://github.com/vlang/vab/blob/master/docs/FAQ.md#generating-mipmap-xxxhdpi-icons-in-the-apkaab) in the [FAQ.md](https://github.com/vlang/vab/blob/master/docs/FAQ.md). +## Package base *overrides* + +*Package base files* can also be provided/tweaked by user application sources +via *their* `platforms/android` directory, or via the explicit `--package-overrides` flag, +which will copy all contents of `--package-overrides ` *on top of* the contents +provided as *package base files* (overwriting any files that may have the same name). +This allows for tweaking certain code bases/setups instead of reshipping complete +copies of *package base files*. + +Also note that special directories named "`java`" in root of projects can act as *implicit* +`--package-overrides`... While this is not ideal, it has historically been a very useful +way for modules/apps to provide tweaks to `vab`'s default *package base files*. + +A similar approach (a special `jni` directory) is being used by the Android NDKs own +tooling (`ndk-build`) for various reasons and can thus be [found in other projects](https://github.com/libsdl-org/SDL/tree/main/android-project/app/jni) +where it serves somewhat similar purposes. + +*`vab` does not treat any `jni` directories specially*, only the above mentioned to +minimize any further confusion. + # Examples The following are some useful examples, please contribute to this section if you think something