// Copyright (c) 2015, Daniel Martí // See LICENSE for licensing information package fdroid import ( "encoding/json" "encoding/xml" "fmt" "html" "io" "sort" "strconv" "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:"flattrID"` SugVersName string `json:"suggestedVersionName"` SugVersCode int `json:"suggestedVersionCode,string"` FdroidRepoName string `json:"-"` FdroidRepoURL string `json:"-"` Localized map[string]Localization `json:"localized"` Apks []*Apk `json:"-"` } type Localization struct { Name string `json:"name"` 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 StringInt `json:"minSdkVersion"` MaxSdk StringInt `json:"maxSdkVersion"` TargetSdk StringInt `json:"targetSdkVersion"` 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 []Permission `json:"uses-permission"` Feats []string `json:"features"` Hash HexVal `json:"hash"` HashType string `json:"hashType"` AppID string `json:"-"` RepoURL string `json:"-"` } type Permission struct { Name string MaxSdk string } func (p *Permission) UnmarshalJSON(data []byte) error { var v []interface{} if err := json.Unmarshal(data, &v); err != nil { fmt.Printf("Error while decoding %v\n", err) return err } p.Name = string(v[0].(string)) if v[1] == nil { p.MaxSdk = "" } else { p.MaxSdk = fmt.Sprint(v[1].(float64)) } return nil } // https://codesahara.com/blog/golang-cannot-unmarshal-string-into-go-value-of-type-int/ type StringInt struct { Value int } func (st *StringInt) UnmarshalJSON(b []byte) error { // convert the bytes into an interface // this will help us check the type of our value // if it is a string that can be converted into an int we convert it // otherwise we return an error var item interface{} if err := json.Unmarshal(b, &item); err != nil { return err } switch v := item.(type) { case int: *st = StringInt{Value: v} case float64: *st = StringInt{Value: int(v)} case string: // here convert the string into an integer i, err := strconv.Atoi(v) if err != nil { // the string might not be of integer type, so return an error return err } *st = StringInt{Value: i} } return nil } 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.Value && (a.MaxSdk.Value == 0 || sdk <= a.MaxSdk.Value) } 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.Name == "" && enOK { app.Name = english.Name } // TODO: why does the json index contain html escapes? app.Name = html.UnescapeString(app.Name) 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 // TODO: why does the json index contain html escapes? apk.VersName = html.UnescapeString(apk.VersName) 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 }