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>
diff --git a/cmd/gomobile/build_androidapp.go b/cmd/gomobile/build_androidapp.go
index 8d261f3..6bbdf8e 100644
--- a/cmd/gomobile/build_androidapp.go
+++ b/cmd/gomobile/build_androidapp.go
@@ -9,6 +9,7 @@
"crypto/x509"
"encoding/base64"
"encoding/pem"
+ "encoding/xml"
"errors"
"fmt"
"go/build"
@@ -19,6 +20,8 @@
"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 @@
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 @@
}
// 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 @@
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 @@
}
}
+ 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 {
diff --git a/cmd/gomobile/writer.go b/cmd/gomobile/writer.go
index 616f267..0f3c29b 100644
--- a/cmd/gomobile/writer.go
+++ b/cmd/gomobile/writer.go
@@ -74,8 +74,6 @@
"fmt"
"hash"
"io"
-
- "golang.org/x/mobile/internal/binres"
)
// NewWriter returns a new Writer writing an APK file to w.
@@ -103,14 +101,6 @@
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 @@
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,
diff --git a/internal/binres/binres.go b/internal/binres/binres.go
index f8da0a8..7b5c3bf 100644
--- a/internal/binres/binres.go
+++ b/internal/binres/binres.go
@@ -160,6 +160,38 @@
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 @@
// 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 @@
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 @@
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)
diff --git a/internal/binres/binres_test.go b/internal/binres/binres_test.go
index 2e80233..6fba715 100644
--- a/internal/binres/binres_test.go
+++ b/internal/binres/binres_test.go
@@ -7,6 +7,7 @@
import (
"bytes"
"encoding"
+ "encoding/xml"
"fmt"
"io/ioutil"
"log"
@@ -209,7 +210,7 @@
t.Fatal(err)
}
- bx, err := UnmarshalXML(f)
+ bx, err := UnmarshalXML(f, false)
if err != nil {
t.Fatal(err)
}
@@ -236,7 +237,7 @@
}
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 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) }
diff --git a/internal/binres/pool.go b/internal/binres/pool.go
index bb9e89d..5e5958d 100644
--- a/internal/binres/pool.go
+++ b/internal/binres/pool.go
@@ -65,6 +65,16 @@
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 }
diff --git a/internal/binres/table.go b/internal/binres/table.go
index 22b987a..6c80aff 100644
--- a/internal/binres/table.go
+++ b/internal/binres/table.go
@@ -56,6 +56,40 @@
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 @@
}
// 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 @@
}
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) 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 @@
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) 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)))
diff --git a/internal/binres/testdata/bootstrap-res/mipmap-xxxhdpi/icon.png b/internal/binres/testdata/bootstrap-res/mipmap-xxxhdpi/icon.png
new file mode 100644
index 0000000..c0745e6
--- /dev/null
+++ b/internal/binres/testdata/bootstrap-res/mipmap-xxxhdpi/icon.png
Binary files differ
diff --git a/internal/binres/testdata/bootstrap.arsc b/internal/binres/testdata/bootstrap.arsc
new file mode 100644
index 0000000..60c1dda
--- /dev/null
+++ b/internal/binres/testdata/bootstrap.arsc
Binary files differ
diff --git a/internal/binres/testdata/bootstrap.bin b/internal/binres/testdata/bootstrap.bin
index ba6a240..35d3df6 100644
--- a/internal/binres/testdata/bootstrap.bin
+++ b/internal/binres/testdata/bootstrap.bin
Binary files differ
diff --git a/internal/binres/testdata/bootstrap.xml b/internal/binres/testdata/bootstrap.xml
index a169737..88df676 100644
--- a/internal/binres/testdata/bootstrap.xml
+++ b/internal/binres/testdata/bootstrap.xml
@@ -17,6 +17,7 @@
android:label="Balloon世界"
android:allowBackup="true"
android:hasCode="false"
+ android:icon="@mipmap/icon"
foo="bar"
android:debuggable="true"
baz="bar"
diff --git a/internal/binres/testdata/gen.sh b/internal/binres/testdata/gen.sh
index 91fd856..e37f21b 100755
--- a/internal/binres/testdata/gen.sh
+++ b/internal/binres/testdata/gen.sh
@@ -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 @@
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