Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

package: support generating icon mipmap-(xxx)(h/m)dpi entries via --icon-mipmaps #328

Merged
merged 11 commits into from
Oct 15, 2024
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
Loading