diff --git a/README.md b/README.md index 60964cf..d161363 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ ## Features - [x] show device selection when multi device connected - [x] screenshot -- [ ] install support http url -- [ ] support launch after install apk +- [x] install support http url +- [x] support launch after install apk - [ ] show wlan (ip,mac,signal), enable and disable it ## Install @@ -22,16 +22,57 @@ brew install codeskyblue/tap/fa download binary from [**releases**](https://github.com/codeskyblue/fa/releases) ## Usage -Screenshot +Show version + +```bash +$ fa version +fa version v0.0.5 # just example +``` + +Screenshot (only png support for now) ```bash fa screenshot -o screenshot.png ``` -~~Install APK~~ +Install APK + +``` +$ fa install ApiDemos-debug.apk +``` + +Install APK then start app + +``` +$ fa install --launch ApiDemos-debug.apk +``` + +Install APK from URL with _uninstall first and launch after installed_ + + +``` +$ fa install --force --launch https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk +Downloading ApiDemos-debug.apk... + 2.94 MiB / 2.94 MiB [================================] 100.00% 282.47 KiB/s 10s +Download saved to ApiDemos-debug.apk ++ adb -s 0123456789ABCDEF uninstall io.appium.android.apis ++ adb -s 0123456789ABCDEF install ApiDemos-debug.apk +ApiDemos-debug.apk: 1 file pushed. 4.8 MB/s (3084877 bytes in 0.609s) + pkg: /data/local/tmp/ApiDemos-debug.apk +Success +Launch io.appium.android.apis ... ++ adb -s 0123456789ABCDEF shell am start -n io.appium.android.apis/.ApiDemos +``` + +Run adb command, if multi device connected, `fa` will give you choice to select one. ``` -fa install https://example.org/demo.apk +$ fa adb pwd +@ select device + > 3aff8912 Smartion + vv12afvv Google Nexus 5 +{selected 3aff8912} +/ ``` ## Reference diff --git a/adb.go b/adb.go new file mode 100644 index 0000000..6c19419 --- /dev/null +++ b/adb.go @@ -0,0 +1,118 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "strconv" + "strings" +) + +const ( + _OKAY = "OKAY" + _FAIL = "FAIL" +) + +func adbCommand(serial string, args ...string) *exec.Cmd { + fmt.Println("+ adb", "-s", serial, strings.Join(args, " ")) + c := exec.Command(adbPath(), args...) + c.Env = append(os.Environ(), "ANDROID_SERIAL="+serial) + return c +} + +func panicError(e error) { + if e != nil { + panic(e) + } +} + +type AdbConnection struct { + net.Conn +} + +// SendPacket data is like "000chost:version" +func (conn *AdbConnection) SendPacket(data string) error { + pktData := fmt.Sprintf("%04x%s", len(data), data) + _, err := conn.Write([]byte(pktData)) + return err +} + +func (conn *AdbConnection) readN(n int) (v string, err error) { + buf := make([]byte, n) + _, err = io.ReadFull(conn, buf) + if err != nil { + return + } + return string(buf), nil +} + +func (conn *AdbConnection) readString() (string, error) { + hexlen, err := conn.readN(4) + if err != nil { + return "", err + } + var length int + _, err = fmt.Sscanf(hexlen, "%04x", &length) + if err != nil { + return "", err + } + return conn.readN(length) +} + +// RecvPacket receive data like "OKAY00040028" +func (conn *AdbConnection) RecvPacket() (data string, err error) { + stat, err := conn.readN(4) + if err != nil { + return "", err + } + switch stat { + case _OKAY: + return conn.readString() + case _FAIL: + data, err = conn.readString() + if err != nil { + return + } + err = errors.New(data) + return + default: + return "", fmt.Errorf("Unknown stat: %s", strconv.Quote(stat)) + } +} + +type AdbClient struct { + Addr string +} + +func NewAdbClient() *AdbClient { + return &AdbClient{ + Addr: "127.0.0.1:5037", + } +} + +var DefaultAdbClient = &AdbClient{ + Addr: "127.0.0.1:5037", +} + +func (c *AdbClient) newConnection() (conn *AdbConnection, err error) { + netConn, err := net.Dial("tcp", c.Addr) + if err != nil { + return nil, err + } + return &AdbConnection{netConn}, nil +} + +// Version returns adb server version +func (c *AdbClient) Version() (string, error) { + conn, err := c.newConnection() + if err != nil { + return "", err + } + if err := conn.SendPacket("host:version"); err != nil { + return "", err + } + return conn.RecvPacket() +} diff --git a/go.mod b/go.mod index 72867d3..a52932e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,12 @@ module github.com/codeskyblue/fa require ( + github.com/cavaliercoder/grab v2.0.0+incompatible github.com/manifoldco/promptui v0.3.2 - gopkg.in/urfave/cli.v1 v1.20.0 + github.com/mattn/go-runewidth v0.0.3 // indirect + github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 + github.com/pkg/errors v0.8.0 + github.com/shogo82148/androidbinary v0.0.0-20180627093851-01c4bfa8b3b5 + github.com/urfave/cli v1.20.0 + gopkg.in/cheggaaa/pb.v1 v1.0.25 ) diff --git a/install.go b/install.go new file mode 100644 index 0000000..17d57c5 --- /dev/null +++ b/install.go @@ -0,0 +1,117 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "strings" + "time" + + "github.com/cavaliercoder/grab" + "github.com/pkg/errors" + "github.com/shogo82148/androidbinary/apk" + "github.com/urfave/cli" + pb "gopkg.in/cheggaaa/pb.v1" +) + +func httpDownload(dst string, url string) (resp *grab.Response, err error) { + client := grab.NewClient() + req, err := grab.NewRequest(dst, url) + if err != nil { + return nil, err + } + // start download + resp = client.Do(req) + fmt.Printf("Downloading %v...\n", resp.Filename) + + // start UI loop + t := time.NewTicker(500 * time.Millisecond) + defer t.Stop() + + bar := pb.New(int(resp.Size)) + bar.SetMaxWidth(80) + bar.ShowSpeed = true + bar.ShowTimeLeft = false + bar.SetUnits(pb.U_BYTES) + bar.Start() + +Loop: + for { + select { + case <-t.C: + bar.Set(int(resp.BytesComplete())) + case <-resp.Done: + bar.Set(int(resp.Size)) + bar.Finish() + break Loop + } + } + // check for errors + if err := resp.Err(); err != nil { + return nil, errors.Wrap(err, "download failed") + } + fmt.Println("Download saved to", resp.Filename) + return resp, err +} + +func actInstall(ctx *cli.Context) error { + if !ctx.Args().Present() { + return errors.New("apkfile or apkurl should provided") + } + serial, err := chooseOne() + if err != nil { + return err + } + arg := ctx.Args().First() + + // download apk + apkpath := arg + if regexp.MustCompile(`^https?://`).MatchString(arg) { + resp, err := httpDownload(".", arg) + if err != nil { + return err + } + apkpath = resp.Filename + } + + // parse apk + pkg, err := apk.OpenFile(apkpath) + if err != nil { + return err + } + + // handle --force + if ctx.Bool("force") { + pkgName := pkg.PackageName() + adbCommand(serial, "uninstall", pkgName).Run() + } + + // install + outBuffer := bytes.NewBuffer(nil) + c := adbCommand(serial, "install", apkpath) + c.Stdout = io.MultiWriter(os.Stdout, outBuffer) + c.Stderr = os.Stderr + if err := c.Run(); err != nil { + return err + } + + if strings.Contains(outBuffer.String(), "Failure") { + return errors.New("install failed") + } + if ctx.Bool("launch") { + packageName := pkg.PackageName() + mainActivity, er := pkg.MainActivity() + if er != nil { + fmt.Println("apk have no main-activity") + return nil + } + if !strings.Contains(mainActivity, ".") { + mainActivity = "." + mainActivity + } + fmt.Println("Launch app", packageName, "...") + adbCommand(serial, "shell", "am", "start", "-n", packageName+"/"+mainActivity).Run() + } + return nil +} diff --git a/main.go b/main.go index 2a14c29..01bd076 100644 --- a/main.go +++ b/main.go @@ -7,11 +7,12 @@ import ( "os" "os/exec" "regexp" + "runtime" "strings" "syscall" "github.com/manifoldco/promptui" - cli "gopkg.in/urfave/cli.v1" + "github.com/urfave/cli" ) var ( @@ -117,14 +118,21 @@ func adbWrap(args ...string) { } func adbPath() string { - return "adb" + exeName := "adb" + if runtime.GOOS == "windows" { + exeName += ".exe" + } + path, err := exec.LookPath(exeName) + if err != nil { + panic(err) + } + return path } func main() { app := cli.NewApp() - app.Name = "ya" app.Version = version - app.Usage = "ya: your adb helps you win at adb" + app.Usage = "fa (fast adb) helps you win at adb" app.Authors = []cli.Author{ cli.Author{ Name: "codeskyblue", @@ -136,13 +144,20 @@ func main() { Name: "version", Usage: "show version", Action: func(ctx *cli.Context) error { - fmt.Printf("[ya]\n version %s\n", version) - fmt.Println("[adb]") - c := exec.Command(adbPath(), "version") - c.Stdout = os.Stdout - c.Stderr = os.Stderr - c.Run() + fmt.Printf("fa version %s\n", version) + adbVersion, err := DefaultAdbClient.Version() + if err != nil { + fmt.Printf("adb version err: %v\n", err) + return err + } + fmt.Println("adb version", adbVersion) + fmt.Println("adb path", adbPath()) return nil + // output, err := exec.Command(adbPath(), "version").Output() + // for _, line := range strings.Split(string(output), "\n") { + // fmt.Println(" " + line) + // } + // return err }, }, { @@ -170,6 +185,23 @@ func main() { }, Action: actScreenshot, }, + { + Name: "install", + Usage: "install apk", + UsageText: "fa install [ul] ", + // UseShortOptionHandling: true, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "uninstall if already installed", + }, + cli.BoolFlag{ + Name: "launch, l", + Usage: "launch after success installed", + }, + }, + Action: actInstall, + }, } err := app.Run(os.Args) if err != nil { diff --git a/screenshot.go b/screenshot.go index 92cffbf..58b621a 100644 --- a/screenshot.go +++ b/screenshot.go @@ -3,46 +3,47 @@ package main import ( "log" "os" - "os/exec" - cli "gopkg.in/urfave/cli.v1" + "github.com/pkg/browser" + "github.com/urfave/cli" ) -func adbCommand(serial string, args ...string) *exec.Cmd { - c := exec.Command(adbPath(), args...) - c.Env = append(os.Environ(), "ANDROID_SERIAL="+serial) - return c +func anyFuncs(funcs ...func() error) error { + var err error + for _, f := range funcs { + if err = f(); err == nil { + return nil + } + } + return err } -func screenshotExecOut(serial, output string) error { - serial, err := chooseOne() - if err != nil { - return err - } - c := adbCommand(serial, "exec-out", "screencap", "-p") - imgfile, err := os.Create(output) - if err != nil { - return err - } - defer func() { - imgfile.Close() +func takeScreenshot(serial, output string) error { + execOut := func() error { + c := adbCommand(serial, "exec-out", "screencap", "-p") + imgfile, err := os.Create(output) if err != nil { - os.Remove(output) + return err } - }() - c.Stdout = imgfile - // c.Stderr = os.Stderr - return c.Run() -} - -func screenshotScreencap(serial, output string) error { - tmpPath := "/sdcard/fa-screenshot.png" - c := adbCommand(serial, "shell", "screencap", "-p", tmpPath) - if err := c.Run(); err != nil { - return err + defer func() { + imgfile.Close() + if err != nil { + os.Remove(output) + } + }() + c.Stdout = imgfile + return c.Run() } - defer adbCommand(serial, "shell", "rm", tmpPath).Run() - return adbCommand(serial, "pull", tmpPath, output).Run() + screencap := func() error { + tmpPath := "/sdcard/fa-screenshot.png" + c := adbCommand(serial, "shell", "screencap", "-p", tmpPath) + if err := c.Run(); err != nil { + return err + } + defer adbCommand(serial, "shell", "rm", tmpPath).Run() + return adbCommand(serial, "pull", tmpPath, output).Run() + } + return anyFuncs(execOut, screencap) } func actScreenshot(ctx *cli.Context) (err error) { @@ -51,18 +52,12 @@ func actScreenshot(ctx *cli.Context) (err error) { return err } output := ctx.String("output") - err = screenshotExecOut(serial, output) - if err != nil { - // log.Println("FAIL:", "exec-out", "screencap") - err = screenshotScreencap(serial, output) - } + err = takeScreenshot(serial, output) if err == nil { - // log.Println("OKAY:", "shell", "screencap") - log.Println("save to", output) + log.Println("saved to", output) if ctx.Bool("open") { - // TODO(ssx): only works on mac - exec.Command("open", output).Run() + browser.OpenFile(output) } } - return + return err }