2024-07-23 10:23:43 +08:00
|
|
|
package apk
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2024-07-31 16:49:14 +08:00
|
|
|
"gitea.bvbej.com/bvbej/base-golang/pkg/android_binary"
|
2024-07-23 10:23:43 +08:00
|
|
|
"image"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
_ "image/jpeg" // handle jpeg format
|
|
|
|
_ "image/png" // handle png format
|
|
|
|
)
|
|
|
|
|
|
|
|
// Apk is an application package file for android.
|
|
|
|
type Apk struct {
|
|
|
|
f *os.File
|
|
|
|
zipreader *zip.Reader
|
|
|
|
manifest Manifest
|
|
|
|
table *android_binary.TableFile
|
|
|
|
}
|
|
|
|
|
|
|
|
// OpenFile will open the file specified by filename and return Apk
|
|
|
|
func OpenFile(filename string) (apk *Apk, err error) {
|
|
|
|
f, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
f.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
fi, err := f.Stat()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
apk, err = OpenZipReader(f, fi.Size())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
apk.f = f
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// OpenZipReader has same arguments like zip.NewReader
|
|
|
|
func OpenZipReader(r io.ReaderAt, size int64) (*Apk, error) {
|
|
|
|
zipreader, err := zip.NewReader(r, size)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
apk := &Apk{
|
|
|
|
zipreader: zipreader,
|
|
|
|
}
|
|
|
|
if err = apk.parseResources(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = apk.parseManifest(); err != nil {
|
|
|
|
return nil, errorf("parse-manifest: %w", err)
|
|
|
|
}
|
|
|
|
return apk, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close is avaliable only if apk is created with OpenFile
|
|
|
|
func (k *Apk) Close() error {
|
|
|
|
if k.f == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return k.f.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Icon returns the icon image of the APK.
|
|
|
|
func (k *Apk) Icon(resConfig *android_binary.ResTableConfig) (image.Image, error) {
|
|
|
|
iconPath, err := k.manifest.App.Icon.WithResTableConfig(resConfig).String()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if android_binary.IsResID(iconPath) {
|
|
|
|
return nil, newError("unable to convert icon-id to icon path")
|
|
|
|
}
|
|
|
|
imgData, err := k.readZipFile(iconPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
m, _, err := image.Decode(bytes.NewReader(imgData))
|
|
|
|
return m, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Label returns the label of the APK.
|
|
|
|
func (k *Apk) Label(resConfig *android_binary.ResTableConfig) (s string, err error) {
|
|
|
|
s, err = k.manifest.App.Label.WithResTableConfig(resConfig).String()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if android_binary.IsResID(s) {
|
|
|
|
err = newError("unable to convert label-id to string")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Manifest returns the manifest of the APK.
|
|
|
|
func (k *Apk) Manifest() Manifest {
|
|
|
|
return k.manifest
|
|
|
|
}
|
|
|
|
|
|
|
|
// PackageName returns the package name of the APK.
|
|
|
|
func (k *Apk) PackageName() string {
|
|
|
|
return k.manifest.Package.MustString()
|
|
|
|
}
|
|
|
|
|
|
|
|
func isMainIntentFilter(intent ActivityIntentFilter) bool {
|
|
|
|
ok := false
|
|
|
|
for _, action := range intent.Actions {
|
|
|
|
s, err := action.Name.String()
|
|
|
|
if err == nil && s == "android.intent.action.MAIN" {
|
|
|
|
ok = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
ok = false
|
|
|
|
for _, category := range intent.Categories {
|
|
|
|
s, err := category.Name.String()
|
|
|
|
if err == nil && s == "android.intent.category.LAUNCHER" {
|
|
|
|
ok = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
|
|
|
// MainActivity returns the name of the main activity.
|
|
|
|
func (k *Apk) MainActivity() (activity string, err error) {
|
|
|
|
for _, act := range k.manifest.App.Activities {
|
|
|
|
for _, intent := range act.IntentFilters {
|
|
|
|
if isMainIntentFilter(intent) {
|
|
|
|
return act.Name.String()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, act := range k.manifest.App.ActivityAliases {
|
|
|
|
for _, intent := range act.IntentFilters {
|
|
|
|
if isMainIntentFilter(intent) {
|
|
|
|
return act.TargetActivity.String()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", newError("No main activity found")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *Apk) parseManifest() error {
|
|
|
|
xmlData, err := k.readZipFile("AndroidManifest.xml")
|
|
|
|
if err != nil {
|
|
|
|
return errorf("failed to read AndroidManifest.xml: %w", err)
|
|
|
|
}
|
|
|
|
xmlfile, err := android_binary.NewXMLFile(bytes.NewReader(xmlData))
|
|
|
|
if err != nil {
|
|
|
|
return errorf("failed to parse AndroidManifest.xml: %w", err)
|
|
|
|
}
|
|
|
|
return xmlfile.Decode(&k.manifest, k.table, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *Apk) parseResources() (err error) {
|
|
|
|
resData, err := k.readZipFile("resources.arsc")
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
k.table, err = android_binary.NewTableFile(bytes.NewReader(resData))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *Apk) readZipFile(name string) (data []byte, err error) {
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
for _, file := range k.zipreader.File {
|
|
|
|
if file.Name != name {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
rc, er := file.Open()
|
|
|
|
if er != nil {
|
|
|
|
err = er
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer rc.Close()
|
|
|
|
_, err = io.Copy(buf, rc)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("File %s not found", strconv.Quote(name))
|
|
|
|
}
|