Skip to content

Commit

Permalink
package: support generating icon mipmap-(xxx)(h/m)dpi entries via `…
Browse files Browse the repository at this point in the history
…--icon-mipmaps` (#328)
  • Loading branch information
larpon authored Oct 15, 2024
1 parent b8a3a6c commit 105c380
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <int> --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 <int> --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.
Expand Down
121 changes: 101 additions & 20 deletions android/package.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module android

import os
import stbi
import regex
import semver
import vab.java
Expand All @@ -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 {
Expand All @@ -43,6 +45,7 @@ pub:
package_id string
activity_name string
icon string
icon_mipmaps bool
version_code int
v_flags []string
input string
Expand All @@ -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 != '' {
Expand Down Expand Up @@ -927,27 +937,21 @@ 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:
// * Creating the directory structures that are returned
// * 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}') }
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions cli/cli.v
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down
2 changes: 2 additions & 0 deletions cli/options.v
Original file line number Diff line number Diff line change
Expand Up @@ -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)']
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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*

Expand Down
19 changes: 19 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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.
Expand Down
53 changes: 33 additions & 20 deletions docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` *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 <path>` *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
Expand Down

0 comments on commit 105c380

Please sign in to comment.