diff --git a/README.md b/README.md index 073a52e..2ec95d5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ settings. * Index verification via jar signature - currently relies on HTTPS * Interaction with multiple devices at once - * Device compatibility filters (minSdk, maxSdk, arch, hardware features) + * Hardware features filtering ### Advantages over the Android client diff --git a/cmd/fdroidcl/download.go b/cmd/fdroidcl/download.go index ece7c64..91a87a9 100644 --- a/cmd/fdroidcl/download.go +++ b/cmd/fdroidcl/download.go @@ -26,12 +26,14 @@ func runDownload(args []string) { } apps := findApps(args) for _, app := range apps { - apk := app.CurApk() - if apk == nil { - log.Fatalf("No current apk found for %s", app.ID) + apks := app.SuggestedApks() + if len(apks) == 0 { + log.Fatalf("No suggested APKs found for %s", app.ID) + } + for _, apk := range apks { + path := downloadApk(&apk) + fmt.Printf("APK available in %s\n", path) } - path := downloadApk(apk) - fmt.Printf("APK available in %s\n", path) } } diff --git a/cmd/fdroidcl/install.go b/cmd/fdroidcl/install.go index 0835950..83b0e18 100644 --- a/cmd/fdroidcl/install.go +++ b/cmd/fdroidcl/install.go @@ -43,9 +43,9 @@ func downloadAndDo(apps []*fdroidcl.App, device *adb.Device, doApk func(*adb.Dev } toInstall := make([]downloaded, len(apps)) for i, app := range apps { - apk := app.CurApk() + apk := app.SuggestedApk(device) if apk == nil { - log.Fatalf("No current apk found for %s", app.ID) + log.Fatalf("No suitable APKs found for %s", app.ID) } path := downloadApk(apk) toInstall[i] = downloaded{apk: apk, path: path} diff --git a/cmd/fdroidcl/search.go b/cmd/fdroidcl/search.go index 83af8eb..2c4d202 100644 --- a/cmd/fdroidcl/search.go +++ b/cmd/fdroidcl/search.go @@ -48,12 +48,11 @@ func runSearch(args []string) { device = mustOneDevice() } apps := filterAppsSearch(mustLoadIndexes(), args) - instPkgs := mustInstalled(device) if *installed { - apps = filterAppsInstalled(apps, instPkgs) + apps = filterAppsInstalled(apps, device) } if *updates { - apps = filterAppsUpdates(apps, instPkgs) + apps = filterAppsUpdates(apps, device) } if *category != "" { apps = filterAppsCategory(apps, *category) @@ -70,7 +69,7 @@ func runSearch(args []string) { fmt.Println(app.ID) } } else { - printApps(apps, instPkgs) + printApps(apps, device) } } @@ -108,44 +107,41 @@ fieldLoop: return false } -func printApps(apps []fdroidcl.App, inst map[string]adb.Package) { +func printApps(apps []fdroidcl.App, device *adb.Device) { maxIDLen := 0 for _, app := range apps { if len(app.ID) > maxIDLen { maxIDLen = len(app.ID) } } + inst := mustInstalled(device) for _, app := range apps { var pkg *adb.Package p, e := inst[app.ID] if e { pkg = &p } - printApp(app, maxIDLen, pkg) + printApp(app, maxIDLen, pkg, device) } } -func descVersion(app fdroidcl.App, inst *adb.Package) string { - cur := app.CurApk() - if cur == nil { - return "(no version available)" - } - if inst == nil { - return fmt.Sprintf("%s (%d)", cur.VName, cur.VCode) - } - if inst.VCode < cur.VCode { - return fmt.Sprintf("%s (%d) -> %s (%d)", inst.VName, inst.VCode, - cur.VName, cur.VCode) - } - if !*installed { - return fmt.Sprintf("%s (%d) [installed]", cur.VName, cur.VCode) +func descVersion(app fdroidcl.App, inst *adb.Package, device *adb.Device) string { + // With "-u" or "-i" option there must be a connected device + if *updates || *installed { + suggested := app.SuggestedApk(device) + if suggested != nil && inst.VCode < suggested.VCode { + return fmt.Sprintf("%s (%d) -> %s (%d)", inst.VName, inst.VCode, + suggested.VName, suggested.VCode) + } + return fmt.Sprintf("%s (%d)", inst.VName, inst.VCode) } - return fmt.Sprintf("%s (%d)", cur.VName, cur.VCode) + // Without "-u" or "-i" we only have repositories indices + return fmt.Sprintf("%s (%d)", app.CVName, app.CVCode) } -func printApp(app fdroidcl.App, IDLen int, inst *adb.Package) { +func printApp(app fdroidcl.App, IDLen int, inst *adb.Package, device *adb.Device) { fmt.Printf("%s%s %s - %s\n", app.ID, strings.Repeat(" ", IDLen-len(app.ID)), - app.Name, descVersion(app, inst)) + app.Name, descVersion(app, inst, device)) fmt.Printf(" %s\n", app.Summary) } @@ -160,8 +156,9 @@ func mustInstalled(device *adb.Device) map[string]adb.Package { return inst } -func filterAppsInstalled(apps []fdroidcl.App, inst map[string]adb.Package) []fdroidcl.App { +func filterAppsInstalled(apps []fdroidcl.App, device *adb.Device) []fdroidcl.App { var result []fdroidcl.App + inst := mustInstalled(device) for _, app := range apps { if _, e := inst[app.ID]; !e { continue @@ -171,18 +168,19 @@ func filterAppsInstalled(apps []fdroidcl.App, inst map[string]adb.Package) []fdr return result } -func filterAppsUpdates(apps []fdroidcl.App, inst map[string]adb.Package) []fdroidcl.App { +func filterAppsUpdates(apps []fdroidcl.App, device *adb.Device) []fdroidcl.App { var result []fdroidcl.App + inst := mustInstalled(device) for _, app := range apps { p, e := inst[app.ID] if !e { continue } - cur := app.CurApk() - if cur == nil { + suggested := app.SuggestedApk(device) + if suggested == nil { continue } - if p.VCode >= cur.VCode { + if p.VCode >= suggested.VCode { continue } result = append(result, app) diff --git a/cmd/fdroidcl/show.go b/cmd/fdroidcl/show.go index 3e5aaed..708ce4d 100644 --- a/cmd/fdroidcl/show.go +++ b/cmd/fdroidcl/show.go @@ -69,13 +69,7 @@ func printAppDetailed(app fdroidcl.App) { p("Summary :", "%s", app.Summary) p("Added :", "%s", app.Added.String()) p("Last Updated :", "%s", app.Updated.String()) - cur := app.CurApk() - if cur != nil { - p("Current Version :", "%s (%d)", cur.VName, cur.VCode) - } else { - p("Current Version :", "(no version available)") - } - p("Upstream Version :", "%s (%d)", app.CVName, app.CVCode) + p("Version :", "%s (%d)", app.CVName, app.CVCode) p("License :", "%s", app.License) if app.Categs != nil { p("Categories :", "%s", strings.Join(app.Categs, ", ")) diff --git a/cmd/fdroidcl/upgrade.go b/cmd/fdroidcl/upgrade.go index 68bf633..bc33029 100644 --- a/cmd/fdroidcl/upgrade.go +++ b/cmd/fdroidcl/upgrade.go @@ -33,8 +33,11 @@ func runUpgrade(args []string) { if !e { log.Fatalf("%s is not installed", app.ID) } - cur := app.CurApk() - if p.VCode >= cur.VCode { + suggested := app.SuggestedApk(device) + if suggested == nil { + log.Fatalf("No suitable APKs found for %s", app.ID) + } + if p.VCode >= suggested.VCode { log.Fatalf("%s is up to date", app.ID) } } diff --git a/index.go b/index.go index 6853c59..ad80bd1 100644 --- a/index.go +++ b/index.go @@ -9,6 +9,8 @@ import ( "io" "sort" "strings" + + "github.com/mvdan/adb" ) type Index struct { @@ -81,11 +83,11 @@ func getIconsDir(density IconDensity) string { } func (a *App) IconURLForDensity(density IconDensity) string { - cur := a.CurApk() - if cur == nil { + if len(a.Apks) == 0 { return "" } - return fmt.Sprintf("%s/%s/%s", cur.Repo.URL, getIconsDir(density), a.Icon) + return fmt.Sprintf("%s/%s/%s", a.Apks[0].Repo.URL, + getIconsDir(density), a.Icon) } func (a *App) IconURL() string { @@ -209,6 +211,29 @@ func (a *Apk) SrcURL() string { return fmt.Sprintf("%s/%s", a.Repo.URL, a.SrcName) } +func (apk *Apk) IsCompatibleABI(ABIs []string) bool { + if len(apk.ABIs) == 0 { + return true // APK does not contain native code + } + for i := range apk.ABIs { + for j := range ABIs { + if apk.ABIs[i] == ABIs[j] { + return true + } + } + } + return false +} + +func (apk *Apk) IsCompatibleAPILevel(sdk int) bool { + return sdk >= apk.MinSdk && (apk.MaxSdk == 0 || sdk <= apk.MaxSdk) +} + +func (apk *Apk) IsCompatible(device *adb.Device) bool { + return apk.IsCompatibleABI(device.ABIs) && + apk.IsCompatibleAPILevel(device.APILevel) +} + type AppList []App func (al AppList) Len() int { return len(al) } @@ -242,15 +267,56 @@ func LoadIndexXML(r io.Reader) (*Index, error) { return &index, nil } -func (a *App) CurApk() *Apk { +func (a *App) ApksByVName(vname string) []Apk { + var apks []Apk for i := range a.Apks { - apk := a.Apks[i] + if vname == a.Apks[i].VName { + apks = append(apks, a.Apks[i]) + } + } + return apks +} + +func (a *App) SuggestedVName() string { + for i := range a.Apks { + apk := &a.Apks[i] if a.CVCode >= apk.VCode { - return &apk + return apk.VName + } + } + return "" +} + +func (a *App) SuggestedApks() []Apk { + // No APKs => nothing to suggest + if len(a.Apks) == 0 { + return nil + } + + // First, try to follow CV + apks := a.ApksByVName(a.SuggestedVName()) + if len(apks) > 0 { + return apks + } + + // When CV is missing current version code or it's invalid (no APKs + // match it), use heuristic: find all APKs having the same version + // string as the APK with the greatest version code + return a.ApksByVName(a.Apks[0].VName) +} + +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 } } - if len(a.Apks) > 0 { - return &a.Apks[0] + for i := range a.Apks { + apk := &a.Apks[i] + if apk.IsCompatible(device) { + return apk + } } return nil }