F-Droid desktop client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
6.2 KiB

// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package fdroidcl
import (
"encoding/xml"
"fmt"
"io"
"sort"
"strings"
"github.com/mvdan/fdroidcl/adb"
)
type Index struct {
Repo Repo `xml:"repo"`
Apps []App `xml:"application"`
}
type Repo struct {
Name string `xml:"name,attr"`
PubKey string `xml:"pubkey,attr"`
Timestamp int `xml:"timestamp,attr"`
URL string `xml:"url,attr"`
Version int `xml:"version,attr"`
MaxAge int `xml:"maxage,attr"`
Description string `xml:"description"`
}
// App is an Android application
type App struct {
ID string `xml:"id"`
Name string `xml:"name"`
Summary string `xml:"summary"`
Added DateVal `xml:"added"`
Updated DateVal `xml:"lastupdated"`
Icon string `xml:"icon"`
Desc string `xml:"desc"`
License string `xml:"license"`
Categs CommaList `xml:"categories"`
Website string `xml:"web"`
Source string `xml:"source"`
Tracker string `xml:"tracker"`
Changelog string `xml:"changelog"`
Donate string `xml:"donate"`
Bitcoin string `xml:"bitcoin"`
Litecoin string `xml:"litecoin"`
FlattrID string `xml:"flattr"`
Apks []Apk `xml:"package"`
CVName string `xml:"marketversion"`
CVCode int `xml:"marketvercode"`
}
type IconDensity uint
const (
UnknownDensity IconDensity = 0
LowDensity IconDensity = 120
MediumDensity IconDensity = 160
HighDensity IconDensity = 240
XHighDensity IconDensity = 320
XXHighDensity IconDensity = 480
XXXHighDensity IconDensity = 640
)
func getIconsDir(density IconDensity) string {
if density == UnknownDensity {
return "icons"
}
for _, d := range [...]IconDensity{
XXXHighDensity,
XXHighDensity,
XHighDensity,
HighDensity,
MediumDensity,
} {
if density >= d {
return fmt.Sprintf("icons-%d", d)
}
}
return fmt.Sprintf("icons-%d", LowDensity)
}
func (a *App) IconURLForDensity(density IconDensity) string {
if len(a.Apks) == 0 {
return ""
}
return fmt.Sprintf("%s/%s/%s", a.Apks[0].repo.URL,
getIconsDir(density), a.Icon)
}
func (a *App) IconURL() string {
return a.IconURLForDensity(UnknownDensity)
}
func (a *App) TextDesc(w io.Writer) {
reader := strings.NewReader(a.Desc)
decoder := xml.NewDecoder(reader)
firstParagraph := true
linePrefix := ""
colsUsed := 0
var links []string
linked := false
for {
token, err := decoder.Token()
if err == io.EOF || token == nil {
break
}
switch t := token.(type) {
case xml.StartElement:
switch t.Name.Local {
case "p":
if firstParagraph {
firstParagraph = false
} else {
fmt.Fprintln(w)
}
linePrefix = ""
colsUsed = 0
case "li":
fmt.Fprint(w, "\n *")
linePrefix = " "
colsUsed = 0
case "a":
for _, attr := range t.Attr {
if attr.Name.Local == "href" {
links = append(links, attr.Value)
linked = true
break
}
}
}
case xml.EndElement:
switch t.Name.Local {
case "p", "ul", "ol":
fmt.Fprintln(w)
}
case xml.CharData:
left := string(t)
if linked {
left += fmt.Sprintf("[%d]", len(links)-1)
linked = false
}
limit := 80 - len(linePrefix) - colsUsed
firstLine := true
for len(left) > limit {
last := 0
for i, c := range left {
if i >= limit {
break
}
if c == ' ' {
last = i
}
}
if firstLine {
firstLine = false
limit += colsUsed
} else {
fmt.Fprint(w, linePrefix)
}
fmt.Fprintln(w, left[:last])
left = left[last+1:]
colsUsed = 0
}
if !firstLine {
fmt.Fprint(w, linePrefix)
}
fmt.Fprint(w, left)
colsUsed += len(left)
}
}
if len(links) > 0 {
fmt.Fprintln(w)
for i, link := range links {
fmt.Fprintf(w, "[%d] %s\n", i, link)
}
}
}
// Apk is an Android package
type Apk struct {
VName string `xml:"version"`
VCode int `xml:"versioncode"`
Size int64 `xml:"size"`
MinSdk int `xml:"sdkver"`
MaxSdk int `xml:"maxsdkver"`
ABIs CommaList `xml:"nativecode"`
ApkName string `xml:"apkname"`
SrcName string `xml:"srcname"`
Sig HexVal `xml:"sig"`
Added DateVal `xml:"added"`
Perms CommaList `xml:"permissions"`
Feats CommaList `xml:"features"`
Hash HexHash `xml:"hash"`
AppID string `xml:"-"`
repo *Repo `xml:"-"`
}
func (a *Apk) URL() string {
return fmt.Sprintf("%s/%s", a.repo.URL, a.ApkName)
}
func (a *Apk) SrcURL() string {
return fmt.Sprintf("%s/%s", a.repo.URL, a.SrcName)
}
func (a *Apk) IsCompatibleABI(ABIs []string) bool {
if len(a.ABIs) == 0 {
return true // APK does not contain native code
}
for _, apkABI := range a.ABIs {
for _, abi := range ABIs {
if apkABI == abi {
return true
}
}
}
return false
}
func (a *Apk) IsCompatibleAPILevel(sdk int) bool {
return sdk >= a.MinSdk && (a.MaxSdk == 0 || sdk <= a.MaxSdk)
}
func (a *Apk) IsCompatible(device *adb.Device) bool {
if device == nil {
return true
}
return a.IsCompatibleABI(device.ABIs) &&
a.IsCompatibleAPILevel(device.APILevel)
}
type AppList []App
func (al AppList) Len() int { return len(al) }
func (al AppList) Swap(i, j int) { al[i], al[j] = al[j], al[i] }
func (al AppList) Less(i, j int) bool { return al[i].ID < al[j].ID }
type ApkList []Apk
func (al ApkList) Len() int { return len(al) }
func (al ApkList) Swap(i, j int) { al[i], al[j] = al[j], al[i] }
func (al ApkList) Less(i, j int) bool { return al[i].VCode > al[j].VCode }
func LoadIndexXML(r io.Reader) (*Index, error) {
var index Index
decoder := xml.NewDecoder(r)
if err := decoder.Decode(&index); err != nil {
return nil, err
}
sort.Sort(AppList(index.Apps))
for i := range index.Apps {
app := &index.Apps[i]
sort.Sort(ApkList(app.Apks))
for j := range app.Apks {
apk := &app.Apks[j]
apk.AppID = app.ID
apk.repo = &index.Repo
}
}
return &index, nil
}
func (a *App) SuggestedApk(device *adb.Device) *Apk {
for i := range a.Apks {
apk := &a.Apks[i]
if a.CVCode >= apk.VCode && apk.IsCompatible(device) {
return apk
}
}
// fall back to the first compatible apk
for i := range a.Apks {
apk := &a.Apks[i]
if apk.IsCompatible(device) {
return apk
}
}
return nil
}