2
0
mirror of synced 2025-02-24 07:18:15 +00:00

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:
Daniel Skinner 2016-08-02 04:11:57 -05:00
parent 3ef91fec25
commit eed0461ac2
11 changed files with 227 additions and 49 deletions

View File

@ -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 {

View File

@ -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,

View File

@ -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)

View File

@ -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) }

View File

@ -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 }

View File

@ -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)))

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

BIN
internal/binres/testdata/bootstrap.arsc vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@ -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"

View File

@ -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