// Copyright (c) 2015, Daniel Martí // See LICENSE for licensing information package main import ( "fmt" "os" "regexp" "sort" "strings" "time" "mvdan.cc/fdroidcl/adb" "mvdan.cc/fdroidcl/fdroid" ) var cmdSearch = &Command{ UsageLine: "search []", Short: "Search available apps", } var ( searchQuiet = cmdSearch.Fset.Bool("q", false, "Print package names only") searchInstalled = cmdSearch.Fset.Bool("i", false, "Filter installed apps") searchUpdates = cmdSearch.Fset.Bool("u", false, "Filter apps with updates") searchDays = cmdSearch.Fset.Int("d", 0, "Select apps last updated in the last days; a negative value drops them instead") searchCategory = cmdSearch.Fset.String("c", "", "Filter apps by category") searchSortBy = cmdSearch.Fset.String("o", "", "Sort order (added, updated)") ) func init() { cmdSearch.Run = runSearch } func runSearch(args []string) error { if *searchInstalled && *searchUpdates { return fmt.Errorf("-i is redundant if -u is specified") } sfunc, err := sortFunc(*searchSortBy) if err != nil { return err } apps, err := loadIndexes() if err != nil { return err } if len(apps) > 0 && *searchCategory != "" { apps = filterAppsCategory(apps, *searchCategory) if apps == nil { return fmt.Errorf("no such category: %s", *searchCategory) } } if len(apps) > 0 && len(args) > 0 { apps = filterAppsSearch(apps, args) } var device *adb.Device var inst map[string]adb.Package if *searchInstalled || *searchUpdates { if device, err = oneDevice(); err != nil { return err } if inst, err = device.Installed(); err != nil { return err } } if len(apps) > 0 && *searchInstalled { apps = filterAppsInstalled(apps, inst) } if len(apps) > 0 && *searchUpdates { apps = filterAppsUpdates(apps, inst, device) } if len(apps) > 0 && *searchDays != 0 { apps = filterAppsLastUpdated(apps, *searchDays) } if sfunc != nil { apps = sortApps(apps, sfunc) } if *searchQuiet { for _, app := range apps { fmt.Fprintln(os.Stdout, app.PackageName) } } else { printApps(apps, inst, device) } return nil } func filterAppsSearch(apps []fdroid.App, terms []string) []fdroid.App { regexes := make([]*regexp.Regexp, len(terms)) for i, term := range terms { regexes[i] = regexp.MustCompile(term) } var result []fdroid.App for _, app := range apps { fields := []string{ strings.ToLower(app.PackageName), strings.ToLower(app.Name), strings.ToLower(app.Summary), strings.ToLower(app.Description), } if !appMatches(fields, regexes) { continue } result = append(result, app) } return result } func appMatches(fields []string, regexes []*regexp.Regexp) bool { fieldLoop: for _, field := range fields { for _, regex := range regexes { if !regex.MatchString(field) { continue fieldLoop } } return true } return false } func printApps(apps []fdroid.App, inst map[string]adb.Package, device *adb.Device) { maxIDLen := 0 for _, app := range apps { if len(app.PackageName) > maxIDLen { maxIDLen = len(app.PackageName) } } for _, app := range apps { var pkg *adb.Package p, e := inst[app.PackageName] if e { pkg = &p } printApp(app, maxIDLen, pkg, device) } } func descVersion(app fdroid.App, inst *adb.Package, device *adb.Device) string { if inst != nil { suggested := app.SuggestedApk(device) if suggested != nil && inst.VersCode < suggested.VersCode { return fmt.Sprintf("%s (%d) -> %s (%d)", inst.VersName, inst.VersCode, suggested.VersName, suggested.VersCode) } return fmt.Sprintf("%s (%d)", inst.VersName, inst.VersCode) } return fmt.Sprintf("%s (%d)", app.SugVersName, app.SugVersCode) } func printApp(app fdroid.App, IDLen int, inst *adb.Package, device *adb.Device) { fmt.Printf("%s%s %s - %s\n", app.PackageName, strings.Repeat(" ", IDLen-len(app.PackageName)), app.Name, descVersion(app, inst, device)) fmt.Printf(" %s\n", app.Summary) } func filterAppsInstalled(apps []fdroid.App, inst map[string]adb.Package) []fdroid.App { var result []fdroid.App for _, app := range apps { if _, e := inst[app.PackageName]; !e { continue } result = append(result, app) } return result } func filterAppsUpdates(apps []fdroid.App, inst map[string]adb.Package, device *adb.Device) []fdroid.App { var result []fdroid.App for _, app := range apps { p, e := inst[app.PackageName] if !e { continue } suggested := app.SuggestedApk(device) if suggested == nil { continue } if p.VersCode >= suggested.VersCode { continue } result = append(result, app) } return result } func filterAppsLastUpdated(apps []fdroid.App, days int) []fdroid.App { var result []fdroid.App newer := true if days < 0 { days = -days newer = false } date := time.Now().Truncate(24*time.Hour).AddDate(0, 0, 1-days) for _, app := range apps { if app.Updated.Before(date) == newer { continue } result = append(result, app) } return result } func contains(l []string, s string) bool { for _, s1 := range l { if s1 == s { return true } } return false } func filterAppsCategory(apps []fdroid.App, categ string) []fdroid.App { var result []fdroid.App for _, app := range apps { if !contains(app.Categories, categ) { continue } result = append(result, app) } return result } func cmpAdded(a, b *fdroid.App) bool { return a.Added.Before(b.Added.Time) } func cmpUpdated(a, b *fdroid.App) bool { return a.Updated.Before(b.Updated.Time) } func sortFunc(sortBy string) (func(a, b *fdroid.App) bool, error) { switch sortBy { case "added": return cmpAdded, nil case "updated": return cmpUpdated, nil case "": return nil, nil } return nil, fmt.Errorf("unknown sort order: %s", sortBy) } type appList struct { l []fdroid.App f func(a, b *fdroid.App) bool } func (al appList) Len() int { return len(al.l) } func (al appList) Swap(i, j int) { al.l[i], al.l[j] = al.l[j], al.l[i] } func (al appList) Less(i, j int) bool { return al.f(&al.l[i], &al.l[j]) } func sortApps(apps []fdroid.App, f func(a, b *fdroid.App) bool) []fdroid.App { sort.Sort(appList{l: apps, f: f}) return apps }