From c511c88854c1e2df53b43ddb778741fead8f9363 Mon Sep 17 00:00:00 2001 From: relan Date: Mon, 18 Apr 2016 22:40:15 +0300 Subject: [PATCH 1/9] Introduce APKs suggestions Suggested APK must be compatible with a particular device. This is always one of the set of suggested APKs. This set contains APKs with the same version string but different version codes (flavours of the same version, e.g. targeting different hardware architectures). --- index.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/index.go b/index.go index 6853c59..101cf49 100644 --- a/index.go +++ b/index.go @@ -9,6 +9,8 @@ import ( "io" "sort" "strings" + + "github.com/mvdan/adb" ) type Index struct { @@ -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) } @@ -254,3 +279,57 @@ func (a *App) CurApk() *Apk { } return nil } + +func (a *App) ApksByVName(vname string) []Apk { + var apks []Apk + for i := range a.Apks { + 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.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 + } + } + for i := range a.Apks { + apk := &a.Apks[i] + if apk.IsCompatible(device) { + return apk + } + } + return nil +} From f9270077b684360d8d98d5d1c132a40a5ad5dfdf Mon Sep 17 00:00:00 2001 From: relan Date: Mon, 18 Apr 2016 22:48:46 +0300 Subject: [PATCH 2/9] install: use suggested APK --- cmd/fdroidcl/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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} From bfb5dc8d22f977e733cb23288da8b08a663982df Mon Sep 17 00:00:00 2001 From: relan Date: Mon, 18 Apr 2016 22:49:21 +0300 Subject: [PATCH 3/9] upgrade: use suggested APK --- cmd/fdroidcl/upgrade.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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) } } From b367c5b4a2a06dc68dc676a9561ab1b12870f5a6 Mon Sep 17 00:00:00 2001 From: relan Date: Mon, 18 Apr 2016 22:56:45 +0300 Subject: [PATCH 4/9] show: avoid CurApk() use This command should work without a connected device. But in this case we cannot check APK compatibility and thus cannot suggest any particular APK. --- cmd/fdroidcl/show.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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, ", ")) From e48634ba6a85821f1105ae8bf441e9abf88d74b9 Mon Sep 17 00:00:00 2001 From: relan Date: Mon, 18 Apr 2016 23:07:36 +0300 Subject: [PATCH 5/9] download: fetch all suggested APKs This command is mostly useful when user does not have the device at the moment. In this case we cannot make any assumptions about it. --- cmd/fdroidcl/download.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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) } } From 2d330b5d14016e56571a15bc8450edb7d17e2802 Mon Sep 17 00:00:00 2001 From: relan Date: Tue, 19 Apr 2016 14:11:09 +0300 Subject: [PATCH 6/9] search: consider suggestions When "-u" or "-i" option is specified we have a connected device and can suggest particular APKs. Without those options it's "free mode" and we cannot make any assumptions. --- cmd/fdroidcl/search.go | 54 ++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 28 deletions(-) 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) From 712ea6ddd4fec7bf86aa45a9487fb8da86c5d8f2 Mon Sep 17 00:00:00 2001 From: relan Date: Tue, 19 Apr 2016 14:47:44 +0300 Subject: [PATCH 7/9] Pick app icon from the latest APK It does not really matter that much which APK we take app icon from. They rarely change and it's a question of aestetics. Just take the latest icon version, like we do with app description. --- index.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.go b/index.go index 101cf49..4895bb4 100644 --- a/index.go +++ b/index.go @@ -83,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 { From e36744405bb9bba6f19cf7b9e56ac7b53d0bfc78 Mon Sep 17 00:00:00 2001 From: relan Date: Tue, 19 Apr 2016 16:44:24 +0300 Subject: [PATCH 8/9] Remove unused CurApk() APKs should not be suggested without knowledge about the device they will be installed on. --- index.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/index.go b/index.go index 4895bb4..ad80bd1 100644 --- a/index.go +++ b/index.go @@ -267,19 +267,6 @@ func LoadIndexXML(r io.Reader) (*Index, error) { return &index, nil } -func (a *App) CurApk() *Apk { - for i := range a.Apks { - apk := a.Apks[i] - if a.CVCode >= apk.VCode { - return &apk - } - } - if len(a.Apks) > 0 { - return &a.Apks[0] - } - return nil -} - func (a *App) ApksByVName(vname string) []Apk { var apks []Apk for i := range a.Apks { From 23514cc5bd72cf255fa7766b32935cac58d3188b Mon Sep 17 00:00:00 2001 From: relan Date: Wed, 20 Apr 2016 19:25:52 +0300 Subject: [PATCH 9/9] README: Remove note about ABI and API level filters They are implemented now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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