cmd/gomobile: icon support for android
Provides support for resources.arsc generation enabling the setting of an application icon. If an asset/icon.png is encountered during build, then the resources.arsc is generated to identify a single xxxhdpi resource and the manifest will be updated to reference resource as app icon. References golang/go#9985 Change-Id: I9ef59fff45dcd612a41c479b2c679d22c094ab36 Reviewed-on: https://go-review.googlesource.com/30019 Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
parent
3ef91fec25
commit
eed0461ac2
@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/build"
|
||||
@ -19,6 +20,8 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mobile/internal/binres"
|
||||
)
|
||||
|
||||
func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool, error) {
|
||||
@ -143,15 +146,7 @@ func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool,
|
||||
return nil
|
||||
}
|
||||
|
||||
w, err := apkwCreate("AndroidManifest.xml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := w.Write(manifestData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err = apkwCreate("classes.dex")
|
||||
w, err := apkwCreate("classes.dex")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -184,6 +179,9 @@ func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool,
|
||||
}
|
||||
|
||||
// Add any assets.
|
||||
var arsc struct {
|
||||
iconPath string
|
||||
}
|
||||
assetsDir := filepath.Join(pkg.Dir, "assets")
|
||||
assetsDirExists := true
|
||||
fi, err := os.Stat(assetsDir)
|
||||
@ -213,6 +211,15 @@ func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool,
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rel, err := filepath.Rel(assetsDir, path); rel == "icon.png" && err == nil {
|
||||
arsc.iconPath = path
|
||||
// TODO returning here does not write the assets/icon.png to the final assets output,
|
||||
// making it unavailable via the assets API. Should the file be duplicated into assets
|
||||
// or should assets API be able to retrieve files from the generated resource table?
|
||||
return nil
|
||||
}
|
||||
|
||||
name := "assets/" + path[len(assetsDir)+1:]
|
||||
return apkwWriteFile(name, path)
|
||||
})
|
||||
@ -221,6 +228,46 @@ func goAndroidBuild(pkg *build.Package, androidArchs []string) (map[string]bool,
|
||||
}
|
||||
}
|
||||
|
||||
bxml, err := binres.UnmarshalXML(bytes.NewReader(manifestData), arsc.iconPath != "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate resources.arsc identifying single xxxhdpi icon resource.
|
||||
if arsc.iconPath != "" {
|
||||
pkgname, err := bxml.RawValueByName("manifest", xml.Name{Local: "package"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tbl, name := binres.NewMipmapTable(pkgname)
|
||||
if err := apkwWriteFile(name, arsc.iconPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w, err := apkwCreate("resources.arsc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bin, err := tbl.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := w.Write(bin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
w, err = apkwCreate("AndroidManifest.xml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bin, err := bxml.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := w.Write(bin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: add gdbserver to apk?
|
||||
|
||||
if !buildN {
|
||||
|
@ -74,8 +74,6 @@ import (
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"golang.org/x/mobile/internal/binres"
|
||||
)
|
||||
|
||||
// NewWriter returns a new Writer writing an APK file to w.
|
||||
@ -103,14 +101,6 @@ func (w *Writer) Create(name string) (io.Writer, error) {
|
||||
if err := w.clearCur(); err != nil {
|
||||
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
|
||||
}
|
||||
if name == "AndroidManifest.xml" {
|
||||
w.cur = &fileWriter{
|
||||
name: name,
|
||||
w: new(bytes.Buffer),
|
||||
sha1: sha1.New(),
|
||||
}
|
||||
return w.cur, nil
|
||||
}
|
||||
res, err := w.create(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("apk: Create(%s): %v", name, err)
|
||||
@ -234,24 +224,6 @@ func (w *Writer) clearCur() error {
|
||||
if w.cur == nil {
|
||||
return nil
|
||||
}
|
||||
if w.cur.name == "AndroidManifest.xml" {
|
||||
buf := w.cur.w.(*bytes.Buffer)
|
||||
bxml, err := binres.UnmarshalXML(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := bxml.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := w.create("AndroidManifest.xml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w.manifest = append(w.manifest, manifestEntry{
|
||||
name: w.cur.name,
|
||||
sha1: w.cur.sha1,
|
||||
|
@ -160,6 +160,38 @@ type XML struct {
|
||||
stack []*Element
|
||||
}
|
||||
|
||||
// RawValueByName returns the original raw string value of first matching element attribute, or error if not exists.
|
||||
// Given <manifest package="VAL" ...> then RawValueByName("manifest", xml.Name{Local: "package"}) returns "VAL".
|
||||
func (bx *XML) RawValueByName(elname string, attrname xml.Name) (string, error) {
|
||||
elref, err := bx.Pool.RefByName(elname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nref, err := bx.Pool.RefByName(attrname.Local)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nsref := PoolRef(NoEntry)
|
||||
if attrname.Space != "" {
|
||||
nsref, err = bx.Pool.RefByName(attrname.Space)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
for el := range bx.iterElements() {
|
||||
if el.Name == elref {
|
||||
for _, attr := range el.attrs {
|
||||
// TODO enforce TypedValue DataString constraint?
|
||||
if nsref == attr.NS && nref == attr.Name {
|
||||
return bx.Pool.strings[int(attr.RawValue)], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no matching element %q for attribute %+v found", elname, attrname)
|
||||
}
|
||||
|
||||
const (
|
||||
androidSchema = "http://schemas.android.com/apk/res/android"
|
||||
toolsSchema = "http://schemas.android.com/tools"
|
||||
@ -170,7 +202,7 @@ var skipSynthesize bool
|
||||
|
||||
// UnmarshalXML decodes an AndroidManifest.xml document returning type XML
|
||||
// containing decoded resources.
|
||||
func UnmarshalXML(r io.Reader) (*XML, error) {
|
||||
func UnmarshalXML(r io.Reader, withIcon bool) (*XML, error) {
|
||||
tbl, err := OpenTable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -247,6 +279,25 @@ func UnmarshalXML(r io.Reader) (*XML, error) {
|
||||
|
||||
q = append(q, ltoken{s, line}, ltoken{e, line})
|
||||
}
|
||||
case "application":
|
||||
if !skipSynthesize {
|
||||
for _, attr := range tkn.Attr {
|
||||
if attr.Name.Space == androidSchema && attr.Name.Local == "icon" {
|
||||
return nil, fmt.Errorf("manual declaration of android:icon in AndroidManifest.xml not supported")
|
||||
}
|
||||
}
|
||||
if withIcon {
|
||||
tkn.Attr = append(tkn.Attr,
|
||||
xml.Attr{
|
||||
Name: xml.Name{
|
||||
Space: androidSchema,
|
||||
Local: "icon",
|
||||
},
|
||||
Value: "@mipmap/icon",
|
||||
})
|
||||
}
|
||||
}
|
||||
q = append(q, ltoken{tkn, line})
|
||||
}
|
||||
default:
|
||||
q = append(q, ltoken{tkn, line})
|
||||
@ -371,6 +422,14 @@ func UnmarshalXML(r io.Reader) (*XML, error) {
|
||||
nattr.TypedValue.Type = DataReference
|
||||
dref, err := tbl.RefByName(attr.Value)
|
||||
if err != nil {
|
||||
if strings.HasPrefix(attr.Value, "@mipmap") {
|
||||
// firstDrawableId is a TableRef matching first entry of mipmap spec initialized by NewMipmapTable.
|
||||
// 7f is default package, 02 is mipmap spec, 0000 is first entry; e.g. R.drawable.icon
|
||||
// TODO resource table should generate ids as required.
|
||||
const firstDrawableId = 0x7f020000
|
||||
nattr.TypedValue.Value = firstDrawableId
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
nattr.TypedValue.Value = uint32(dref)
|
||||
|
@ -7,6 +7,7 @@ package binres
|
||||
import (
|
||||
"bytes"
|
||||
"encoding"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -209,7 +210,7 @@ func TestEncode(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bx, err := UnmarshalXML(f)
|
||||
bx, err := UnmarshalXML(f, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -236,7 +237,7 @@ func TestEncode(t *testing.T) {
|
||||
}
|
||||
|
||||
if err := compareElements(bx, bxml); err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Current output byte-for-byte of pkg binres is close, but not exact, to output of aapt.
|
||||
@ -253,6 +254,23 @@ func TestEncode(t *testing.T) {
|
||||
// }
|
||||
}
|
||||
|
||||
func TestRawValueByName(t *testing.T) {
|
||||
f, err := os.Open("testdata/bootstrap.xml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bx, err := UnmarshalXML(f, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkgname, err := bx.RawValueByName("manifest", xml.Name{Local: "package"})
|
||||
if want := "com.zentus.balloon"; err != nil || pkgname != want {
|
||||
t.Fatalf("have (%q, %v), want (%q, nil)", pkgname, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
type byAttrName []*Attribute
|
||||
|
||||
func (a byAttrName) Len() int { return len(a) }
|
||||
|
@ -65,6 +65,16 @@ func (pl *Pool) ref(s string) PoolRef {
|
||||
return PoolRef(len(pl.strings) - 1)
|
||||
}
|
||||
|
||||
// RefByName returns the PoolRef of s, or error if not exists.
|
||||
func (pl *Pool) RefByName(s string) (PoolRef, error) {
|
||||
for i, x := range pl.strings {
|
||||
if s == x {
|
||||
return PoolRef(i), nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("PoolRef by name %q does not exist", s)
|
||||
}
|
||||
|
||||
func (pl *Pool) IsSorted() bool { return pl.flags&SortedFlag == SortedFlag }
|
||||
func (pl *Pool) IsUTF8() bool { return pl.flags&UTF8Flag == UTF8Flag }
|
||||
|
||||
|
@ -56,6 +56,40 @@ type Table struct {
|
||||
pkgs []*Package
|
||||
}
|
||||
|
||||
// NewMipmapTable returns a resource table initialized for a single xxxhdpi mipmap resource
|
||||
// and the path to write resource data to.
|
||||
func NewMipmapTable(pkgname string) (*Table, string) {
|
||||
pkg := &Package{id: 127, name: pkgname, typePool: &Pool{}, keyPool: &Pool{}}
|
||||
|
||||
attr := pkg.typePool.ref("attr")
|
||||
mipmap := pkg.typePool.ref("mipmap")
|
||||
icon := pkg.keyPool.ref("icon")
|
||||
|
||||
nt := &Entry{values: []*Value{{data: &Data{Type: DataString}}}}
|
||||
typ := &Type{id: 2, indices: []uint32{0}, entries: []*Entry{nt}}
|
||||
typ.config.screenType.density = 640
|
||||
typ.config.version.sdk = 4
|
||||
|
||||
pkg.specs = append(pkg.specs,
|
||||
&TypeSpec{
|
||||
id: uint8(attr) + 1, //1,
|
||||
},
|
||||
&TypeSpec{
|
||||
id: uint8(mipmap) + 1, //2,
|
||||
entryCount: 1,
|
||||
entries: []uint32{uint32(icon)}, // {0}
|
||||
types: []*Type{typ},
|
||||
})
|
||||
|
||||
pkg.lastPublicType = uint32(len(pkg.typePool.strings)) // 2
|
||||
pkg.lastPublicKey = uint32(len(pkg.keyPool.strings)) // 1
|
||||
|
||||
name := "res/mipmap-xxxhdpi-v4/icon.png"
|
||||
tbl := &Table{pool: &Pool{}, pkgs: []*Package{pkg}}
|
||||
tbl.pool.ref(name)
|
||||
return tbl, name
|
||||
}
|
||||
|
||||
// OpenSDKTable decodes resources.arsc from sdk platform jar.
|
||||
func OpenSDKTable() (*Table, error) {
|
||||
bin, err := apiResources()
|
||||
@ -89,7 +123,8 @@ func OpenTable() (*Table, error) {
|
||||
}
|
||||
|
||||
// SpecByName parses the spec name from an entry string if necessary and returns
|
||||
// the Package and TypeSpec associated with that name.
|
||||
// the Package and TypeSpec associated with that name along with their respective
|
||||
// indices.
|
||||
//
|
||||
// For example:
|
||||
// tbl.SpecByName("@android:style/Theme.NoTitleBar")
|
||||
@ -261,7 +296,7 @@ func (pkg *Package) UnmarshalBinary(bin []byte) error {
|
||||
}
|
||||
|
||||
buf := bin[idOffset:]
|
||||
for len(pkg.specs) < len(pkg.typePool.strings) {
|
||||
for len(buf) > 0 {
|
||||
t := ResType(btou16(buf))
|
||||
switch t {
|
||||
case ResTableTypeSpec:
|
||||
@ -288,9 +323,11 @@ func (pkg *Package) UnmarshalBinary(bin []byte) error {
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalBinary() ([]byte, error) {
|
||||
bin := make([]byte, 284)
|
||||
// Package header size is determined by C++ struct ResTable_package
|
||||
// see frameworks/base/include/ResourceTypes.h
|
||||
bin := make([]byte, 288)
|
||||
putu16(bin, uint16(ResTablePackage))
|
||||
putu16(bin[2:], 284)
|
||||
putu16(bin[2:], 288)
|
||||
|
||||
putu32(bin[8:], pkg.id)
|
||||
p := utf16.Encode([]rune(pkg.name))
|
||||
@ -532,6 +569,31 @@ func (typ *Type) MarshalBinary() ([]byte, error) {
|
||||
putu32(bin[12:], uint32(len(typ.entries)))
|
||||
putu32(bin[16:], uint32(56+len(typ.entries)*4))
|
||||
|
||||
// assure typ.config.size is always written as 52; extended configuration beyond supported
|
||||
// API level is not supported by this marshal implementation but will be forward-compatible.
|
||||
putu32(bin[20:], 52)
|
||||
|
||||
putu16(bin[24:], typ.config.imsi.mcc)
|
||||
putu16(bin[26:], typ.config.imsi.mnc)
|
||||
putu16(bin[28:], typ.config.locale.language)
|
||||
putu16(bin[30:], typ.config.locale.country)
|
||||
bin[32] = typ.config.screenType.orientation
|
||||
bin[33] = typ.config.screenType.touchscreen
|
||||
putu16(bin[34:], typ.config.screenType.density)
|
||||
bin[36] = typ.config.input.keyboard
|
||||
bin[37] = typ.config.input.navigation
|
||||
bin[38] = typ.config.input.inputFlags
|
||||
bin[39] = typ.config.input.inputPad0
|
||||
putu16(bin[40:], typ.config.screenSize.width)
|
||||
putu16(bin[42:], typ.config.screenSize.height)
|
||||
putu16(bin[44:], typ.config.version.sdk)
|
||||
putu16(bin[46:], typ.config.version.minor)
|
||||
bin[48] = typ.config.screenConfig.layout
|
||||
bin[49] = typ.config.screenConfig.uiMode
|
||||
putu16(bin[50:], typ.config.screenConfig.smallestWidthDP)
|
||||
putu16(bin[52:], typ.config.screenSizeDP.width)
|
||||
putu16(bin[54:], typ.config.screenSizeDP.height)
|
||||
|
||||
var ntbin []byte
|
||||
for i, nt := range typ.entries {
|
||||
if nt == nil { // NoEntry
|
||||
@ -594,11 +656,15 @@ func (nt *Entry) UnmarshalBinary(bin []byte) error {
|
||||
|
||||
func (nt *Entry) MarshalBinary() ([]byte, error) {
|
||||
bin := make([]byte, 8)
|
||||
putu16(bin, nt.size)
|
||||
sz := nt.size
|
||||
if sz == 0 {
|
||||
sz = 8
|
||||
}
|
||||
putu16(bin, sz)
|
||||
putu16(bin[2:], nt.flags)
|
||||
putu32(bin[4:], uint32(nt.key))
|
||||
|
||||
if nt.size == 16 {
|
||||
if sz == 16 {
|
||||
bin = append(bin, make([]byte, 8+len(nt.values)*12)...)
|
||||
putu32(bin[8:], uint32(nt.parent))
|
||||
putu32(bin[12:], uint32(len(nt.values)))
|
||||
|
BIN
internal/binres/testdata/bootstrap-res/mipmap-xxxhdpi/icon.png
vendored
Normal file
BIN
internal/binres/testdata/bootstrap-res/mipmap-xxxhdpi/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 486 B |
BIN
internal/binres/testdata/bootstrap.arsc
vendored
Normal file
BIN
internal/binres/testdata/bootstrap.arsc
vendored
Normal file
Binary file not shown.
BIN
internal/binres/testdata/bootstrap.bin
vendored
BIN
internal/binres/testdata/bootstrap.bin
vendored
Binary file not shown.
1
internal/binres/testdata/bootstrap.xml
vendored
1
internal/binres/testdata/bootstrap.xml
vendored
@ -17,6 +17,7 @@ license that can be found in the LICENSE file.
|
||||
android:label="Balloon世界"
|
||||
android:allowBackup="true"
|
||||
android:hasCode="false"
|
||||
android:icon="@mipmap/icon"
|
||||
foo="bar"
|
||||
android:debuggable="true"
|
||||
baz="bar"
|
||||
|
11
internal/binres/testdata/gen.sh
vendored
11
internal/binres/testdata/gen.sh
vendored
@ -1,4 +1,4 @@
|
||||
#! /usr/bin/sh
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# version of build-tools tests run against
|
||||
AAPT=${ANDROID_HOME}/build-tools/23.0.1/aapt
|
||||
@ -7,9 +7,14 @@ AAPT=${ANDROID_HOME}/build-tools/23.0.1/aapt
|
||||
APIJAR=${ANDROID_HOME}/platforms/android-15/android.jar
|
||||
|
||||
for f in *.xml; do
|
||||
RES=""
|
||||
if [ -d "${f:0:-4}-res" ]; then
|
||||
RES="-S ${f:0:-4}-res"
|
||||
fi
|
||||
cp "$f" AndroidManifest.xml
|
||||
"$AAPT" p -M AndroidManifest.xml -I "$APIJAR" -F tmp.apk
|
||||
unzip -qq -o tmp.apk
|
||||
"$AAPT" p -M AndroidManifest.xml $RES -I "$APIJAR" -F tmp.apk
|
||||
unzip -qq -o tmp.apk AndroidManifest.xml resources.arsc
|
||||
mv AndroidManifest.xml "${f:0:-3}bin"
|
||||
mv resources.arsc "${f:0:-3}arsc"
|
||||
rm tmp.apk
|
||||
done
|
||||
|
Loading…
x
Reference in New Issue
Block a user