|  | // Copyright 2015 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package app_test | 
|  |  | 
|  | import ( | 
|  | "fmt" | 
|  | "image" | 
|  | "image/color" | 
|  | _ "image/png" | 
|  | "io/ioutil" | 
|  | "net" | 
|  | "os" | 
|  | "os/exec" | 
|  | "strings" | 
|  | "testing" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/mobile/app/internal/apptest" | 
|  | "golang.org/x/mobile/event/size" | 
|  | ) | 
|  |  | 
|  | // TestAndroidApp tests the lifecycle, event, and window semantics of a | 
|  | // simple android app. | 
|  | // | 
|  | // Beyond testing the app package, the goal is to eventually have | 
|  | // helper libraries that make tests like these easy to write. Hopefully | 
|  | // having a user of such a fictional package will help illuminate the way. | 
|  | func TestAndroidApp(t *testing.T) { | 
|  | t.Skip("see issue #23835") | 
|  | if _, err := exec.Command("which", "adb").CombinedOutput(); err != nil { | 
|  | t.Skip("command adb not found, skipping") | 
|  | } | 
|  | devicesTxt, err := exec.Command("adb", "devices").CombinedOutput() | 
|  | if err != nil { | 
|  | t.Errorf("adb devices failed: %v: %v", err, devicesTxt) | 
|  | } | 
|  | deviceCount := 0 | 
|  | for _, d := range strings.Split(strings.TrimSpace(string(devicesTxt)), "\n") { | 
|  | if strings.Contains(d, "List of devices") { | 
|  | continue | 
|  | } | 
|  | // TODO(crawshaw): I believe some unusable devices can appear in the | 
|  | // list with note on them, but I cannot reproduce this right now. | 
|  | deviceCount++ | 
|  | } | 
|  | if deviceCount == 0 { | 
|  | t.Skip("no android devices attached") | 
|  | } | 
|  |  | 
|  | run(t, "gomobile", "version") | 
|  |  | 
|  | origWD, err := os.Getwd() | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | tmpdir, err := ioutil.TempDir("", "app-test-") | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | defer os.RemoveAll(tmpdir) | 
|  |  | 
|  | if err := os.Chdir(tmpdir); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | defer os.Chdir(origWD) | 
|  |  | 
|  | run(t, "gomobile", "install", "golang.org/x/mobile/app/internal/testapp") | 
|  |  | 
|  | ln, err := net.Listen("tcp4", "localhost:0") | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | defer ln.Close() | 
|  | localaddr := fmt.Sprintf("tcp:%d", ln.Addr().(*net.TCPAddr).Port) | 
|  | t.Logf("local address: %s", localaddr) | 
|  |  | 
|  | exec.Command("adb", "reverse", "--remove", "tcp:"+apptest.Port).Run() // ignore failure | 
|  | run(t, "adb", "reverse", "tcp:"+apptest.Port, localaddr) | 
|  |  | 
|  | const ( | 
|  | KeycodePower  = "26" | 
|  | KeycodeUnlock = "82" | 
|  | ) | 
|  |  | 
|  | run(t, "adb", "shell", "input", "keyevent", KeycodePower) | 
|  | run(t, "adb", "shell", "input", "keyevent", KeycodeUnlock) | 
|  |  | 
|  | const ( | 
|  | rotationPortrait  = "0" | 
|  | rotationLandscape = "1" | 
|  | ) | 
|  |  | 
|  | rotate := func(rotation string) { | 
|  | run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:user_rotation", "--bind", "value:i:"+rotation) | 
|  | } | 
|  |  | 
|  | // turn off automatic rotation and start in portrait | 
|  | run(t, "adb", "shell", "content", "insert", "--uri", "content://settings/system", "--bind", "name:s:accelerometer_rotation", "--bind", "value:i:0") | 
|  | rotate(rotationPortrait) | 
|  |  | 
|  | // start testapp | 
|  | run(t, | 
|  | "adb", "shell", "am", "start", "-n", | 
|  | "org.golang.testapp/org.golang.app.GoNativeActivity", | 
|  | ) | 
|  |  | 
|  | var conn net.Conn | 
|  | connDone := make(chan struct{}) | 
|  | go func() { | 
|  | conn, err = ln.Accept() | 
|  | connDone <- struct{}{} | 
|  | }() | 
|  |  | 
|  | select { | 
|  | case <-time.After(5 * time.Second): | 
|  | t.Fatal("timeout waiting for testapp to dial host") | 
|  | case <-connDone: | 
|  | if err != nil { | 
|  | t.Fatalf("ln.Accept: %v", err) | 
|  | } | 
|  | } | 
|  | defer conn.Close() | 
|  | comm := &apptest.Comm{ | 
|  | Conn:   conn, | 
|  | Fatalf: t.Fatalf, | 
|  | Printf: t.Logf, | 
|  | } | 
|  |  | 
|  | var pixelsPerPt float32 | 
|  | var orientation size.Orientation | 
|  |  | 
|  | comm.Recv("hello_from_testapp") | 
|  | comm.Send("hello_from_host") | 
|  | comm.Recv("lifecycle_visible") | 
|  | comm.Recv("size", &pixelsPerPt, &orientation) | 
|  | if pixelsPerPt < 0.1 { | 
|  | t.Fatalf("bad pixelsPerPt: %f", pixelsPerPt) | 
|  | } | 
|  |  | 
|  | // A single paint event is sent when the lifecycle enters | 
|  | // StageVisible, and after the end of a touch event. | 
|  | var color string | 
|  | comm.Recv("paint", &color) | 
|  | // Ignore the first paint color, it may be slow making it to the screen. | 
|  |  | 
|  | rotate(rotationLandscape) | 
|  | comm.Recv("size", &pixelsPerPt, &orientation) | 
|  | if want := size.OrientationLandscape; orientation != want { | 
|  | t.Errorf("want orientation %d, got %d", want, orientation) | 
|  | } | 
|  |  | 
|  | var x, y int | 
|  | var ty string | 
|  |  | 
|  | tap(t, 50, 260) | 
|  | comm.Recv("touch", &ty, &x, &y) | 
|  | if ty != "begin" || x != 50 || y != 260 { | 
|  | t.Errorf("want touch begin(50, 260), got %s(%d,%d)", ty, x, y) | 
|  | } | 
|  | comm.Recv("touch", &ty, &x, &y) | 
|  | if ty != "end" || x != 50 || y != 260 { | 
|  | t.Errorf("want touch end(50, 260), got %s(%d,%d)", ty, x, y) | 
|  | } | 
|  |  | 
|  | comm.Recv("paint", &color) | 
|  | if gotColor := currentColor(t); color != gotColor { | 
|  | t.Errorf("app reports color %q, but saw %q", color, gotColor) | 
|  | } | 
|  |  | 
|  | rotate(rotationPortrait) | 
|  | comm.Recv("size", &pixelsPerPt, &orientation) | 
|  | if want := size.OrientationPortrait; orientation != want { | 
|  | t.Errorf("want orientation %d, got %d", want, orientation) | 
|  | } | 
|  |  | 
|  | tap(t, 50, 260) | 
|  | comm.Recv("touch", &ty, &x, &y) // touch begin | 
|  | comm.Recv("touch", &ty, &x, &y) // touch end | 
|  | comm.Recv("paint", &color) | 
|  | if gotColor := currentColor(t); color != gotColor { | 
|  | t.Errorf("app reports color %q, but saw %q", color, gotColor) | 
|  | } | 
|  |  | 
|  | // TODO: lifecycle testing (NOTE: adb shell input keyevent 4 is the back button) | 
|  | } | 
|  |  | 
|  | func currentColor(t *testing.T) string { | 
|  | file := fmt.Sprintf("app-screen-%d.png", time.Now().Unix()) | 
|  |  | 
|  | run(t, "adb", "shell", "screencap", "-p", "/data/local/tmp/"+file) | 
|  | run(t, "adb", "pull", "/data/local/tmp/"+file) | 
|  | run(t, "adb", "shell", "rm", "/data/local/tmp/"+file) | 
|  | defer os.Remove(file) | 
|  |  | 
|  | f, err := os.Open(file) | 
|  | if err != nil { | 
|  | t.Errorf("currentColor: cannot open screencap: %v", err) | 
|  | return "" | 
|  | } | 
|  | m, _, err := image.Decode(f) | 
|  | if err != nil { | 
|  | t.Errorf("currentColor: cannot decode screencap: %v", err) | 
|  | return "" | 
|  | } | 
|  | var center color.Color | 
|  | { | 
|  | b := m.Bounds() | 
|  | x, y := b.Min.X+(b.Max.X-b.Min.X)/2, b.Min.Y+(b.Max.Y-b.Min.Y)/2 | 
|  | center = m.At(x, y) | 
|  | } | 
|  | r, g, b, _ := center.RGBA() | 
|  | switch { | 
|  | case r == 0xffff && g == 0x0000 && b == 0x0000: | 
|  | return "red" | 
|  | case r == 0x0000 && g == 0xffff && b == 0x0000: | 
|  | return "green" | 
|  | case r == 0x0000 && g == 0x0000 && b == 0xffff: | 
|  | return "blue" | 
|  | default: | 
|  | return fmt.Sprintf("indeterminate: %v", center) | 
|  | } | 
|  | } | 
|  |  | 
|  | func tap(t *testing.T, x, y int) { | 
|  | run(t, "adb", "shell", "input", "tap", fmt.Sprintf("%d", x), fmt.Sprintf("%d", y)) | 
|  | } | 
|  |  | 
|  | func run(t *testing.T, cmdName string, arg ...string) { | 
|  | cmd := exec.Command(cmdName, arg...) | 
|  | t.Log(strings.Join(cmd.Args, " ")) | 
|  | out, err := cmd.CombinedOutput() | 
|  | if err != nil { | 
|  | t.Fatalf("%s %v: %s", strings.Join(cmd.Args, " "), err, out) | 
|  | } | 
|  | } |