mirror of https://github.com/mvdan/fdroidcl.git
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.
306 lines
7.0 KiB
306 lines
7.0 KiB
// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc> |
|
// See LICENSE for licensing information |
|
|
|
package fdroid |
|
|
|
import ( |
|
"encoding/json" |
|
"encoding/xml" |
|
"fmt" |
|
"io" |
|
"sort" |
|
"strings" |
|
|
|
"mvdan.cc/fdroidcl/adb" |
|
) |
|
|
|
type Index struct { |
|
Repo Repo `json:"repo"` |
|
Apps []App `json:"apps"` |
|
Packages map[string][]Apk `json:"packages"` |
|
} |
|
|
|
type Repo struct { |
|
Name string `json:"name"` |
|
Timestamp UnixDate `json:"timestamp"` |
|
Address string `json:"address"` |
|
Icon string `json:"icon"` |
|
Version int `json:"version"` |
|
MaxAge int `json:"maxage"` |
|
Description string `json:"description"` |
|
} |
|
|
|
// App is an Android application |
|
type App struct { |
|
PackageName string `json:"packageName"` |
|
Name string `json:"name"` |
|
Summary string `json:"summary"` |
|
Added UnixDate `json:"added"` |
|
Updated UnixDate `json:"lastUpdated"` |
|
Icon string `json:"icon"` |
|
Description string `json:"description"` |
|
License string `json:"license"` |
|
Categories []string `json:"categories"` |
|
Website string `json:"webSite"` |
|
SourceCode string `json:"sourceCode"` |
|
IssueTracker string `json:"issueTracker"` |
|
Changelog string `json:"changelog"` |
|
Donate string `json:"donate"` |
|
Bitcoin string `json:"bitcoin"` |
|
Litecoin string `json:"litecoin"` |
|
FlattrID string `json:"flattr"` |
|
SugVersName string `json:"suggestedVersionName"` |
|
SugVersCode int `json:"suggestedVersionCode,string"` |
|
|
|
Localized map[string]Localization `json:"localized"` |
|
|
|
Apks []*Apk `json:"-"` |
|
} |
|
|
|
type Localization struct { |
|
Summary string `json:"summary"` |
|
Description string `json:"description"` |
|
} |
|
|
|
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].RepoURL, |
|
getIconsDir(density), a.Icon) |
|
} |
|
|
|
func (a *App) IconURL() string { |
|
return a.IconURLForDensity(UnknownDensity) |
|
} |
|
|
|
func (a *App) TextDesc(w io.Writer) { |
|
reader := strings.NewReader(a.Description) |
|
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 { |
|
VersName string `json:"versionName"` |
|
VersCode int `json:"versionCode"` |
|
Size int64 `json:"size"` |
|
MinSdk int `json:"sdkver"` |
|
MaxSdk int `json:"maxsdkver"` |
|
ABIs []string `json:"nativecode"` |
|
ApkName string `json:"apkname"` |
|
SrcName string `json:"srcname"` |
|
Sig HexVal `json:"sig"` |
|
Signer HexVal `json:"signer"` |
|
Added UnixDate `json:"added"` |
|
Perms []string `json:"permissions"` |
|
Feats []string `json:"features"` |
|
Hash HexVal `json:"hash"` |
|
HashType string `json:"hashType"` |
|
|
|
AppID string `json:"-"` |
|
RepoURL string `json:"-"` |
|
} |
|
|
|
func (a *Apk) URL() string { |
|
return fmt.Sprintf("%s/%s", a.RepoURL, a.ApkName) |
|
} |
|
|
|
func (a *Apk) SrcURL() string { |
|
return fmt.Sprintf("%s/%s", a.RepoURL, 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].PackageName < al[j].PackageName } |
|
|
|
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].VersCode > al[j].VersCode } |
|
|
|
func LoadIndexJSON(r io.Reader) (*Index, error) { |
|
var index Index |
|
decoder := json.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] |
|
english, enOK := app.Localized["en"] |
|
if !enOK { |
|
english, enOK = app.Localized["en-US"] |
|
} |
|
if app.Summary == "" && enOK { |
|
app.Summary = english.Summary |
|
} |
|
if app.Description == "" && enOK { |
|
app.Description = english.Description |
|
} |
|
app.Summary = strings.TrimSpace(app.Summary) |
|
sort.Sort(ApkList(index.Packages[app.PackageName])) |
|
for i := range index.Packages[app.PackageName] { |
|
apk := &index.Packages[app.PackageName][i] |
|
apk.AppID = app.PackageName |
|
apk.RepoURL = index.Repo.Address |
|
app.Apks = append(app.Apks, apk) |
|
} |
|
} |
|
return &index, nil |
|
} |
|
|
|
func (a *App) SuggestedApk(device *adb.Device) *Apk { |
|
for _, apk := range a.Apks { |
|
if a.SugVersCode >= apk.VersCode && apk.IsCompatible(device) { |
|
return apk |
|
} |
|
} |
|
// fall back to the first compatible apk |
|
for _, apk := range a.Apks { |
|
if apk.IsCompatible(device) { |
|
return apk |
|
} |
|
} |
|
return nil |
|
}
|
|
|