Skip to content

Commit

Permalink
add creating AVD feature
Browse files Browse the repository at this point in the history
  • Loading branch information
bartekpacia committed Jan 15, 2025
1 parent 8e3d639 commit 080aa07
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 7 deletions.
126 changes: 126 additions & 0 deletions avdmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package emulator

import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"golang.org/x/text/cases"
"golang.org/x/text/language"
)

// CreateAVD creates a new Android Virtual Device and returns its name and path.
//
// It wraps the avdmanager tool from Android SDK.Example AVD manager invocation:
//
// avdmanager create avd \
// --sdcard '8192M' \
// --package "system-images;android-34;google_apis_playstore;arm64-v8a" \
// --name "Pixel_7_API_34" \
// --device "pixel_7"
//
// In addition, it also automatically enables keyboard input.
func CreateAVD(osimage SystemImage, skin string, sdcardMB int) (string, string, error) {
avdName := cases.Title(language.English, cases.NoLower).String(skin)
avdName = fmt.Sprint(avdName, "_API_", osimage.ApiLevel())
args := []string{"create", "avd"}
args = append(args, "--sdcard", strconv.Itoa(sdcardMB))
args = append(args, "--package", string(osimage))
args = append(args, "--name", avdName)
args = append(args, "--device", skin)

var stderr bytes.Buffer
cmd := exec.Command("avdmanager", args...)
printInvocation(cmd)
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", "", fmt.Errorf("failed to run %s: %v, %v", cmd, err, stderr.String())
}

avdPath := filepath.Join(os.Getenv("ANDROID_USER_HOME"), "avd", avdName+".avd")
err = updateConfig(avdPath)
if err != nil {
return "", "", fmt.Errorf("failed to update config %s: %v", avdPath, err)
}

return avdName, avdPath, nil
}

func Skins() ([]string, error) {
var directories []string

androidHome := os.Getenv("ANDROID_HOME")
if androidHome == "" {
return nil, fmt.Errorf("ANDROID_HOME environment variable not set")
}

skinsPath := filepath.Join(androidHome, "skins")

entries, err := os.ReadDir(skinsPath)
if err != nil {
return nil, fmt.Errorf("read directory %s: %v", skinsPath, err)
}

for _, entry := range entries {
if entry.IsDir() {
directories = append(directories, entry.Name())
}
}

return directories, nil
}

// updateConfig updates the config.ini file to enable keyboard support.
func updateConfig(avdDir string) error {
configIniPath := filepath.Join(avdDir, "config.ini")

file, err := os.OpenFile(configIniPath, os.O_RDWR, os.ModePerm)
if err != nil {
return fmt.Errorf("open config.ini file: %v", err)
}
defer file.Close()

var lines []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "hw.keyboard=") {
fmt.Println("HAS PREFIX:!!, line: ", line)
lines = append(lines, "hw.keyboard = yes")
} else {
lines = append(lines, line)
}
}

err = scanner.Err()
if err != nil {
return fmt.Errorf("scanning %s: %v", configIniPath, err)
}

err = os.Truncate(configIniPath, 0)
if err != nil {
return fmt.Errorf("truncating %s: %v", configIniPath, err)
}

writer := bufio.NewWriter(file)
for _, line := range lines {
_, err := writer.WriteString(line + "\n")
if err != nil {
return fmt.Errorf("writing %s: %v", configIniPath, err)
}
}

err = writer.Flush()
if err != nil {
return fmt.Errorf("flushing %s: %v", configIniPath, err)
}

return nil
}
85 changes: 85 additions & 0 deletions cmd/emu/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"runtime"
"syscall"

emulator "github.com/bartekpacia/emu"
Expand Down Expand Up @@ -49,6 +50,7 @@ func main() {
&displaysizeCommand,
&animationsCommand,
// manage
&createCommand,
&runCommand,
&listCommand,
&killCommand,
Expand All @@ -66,6 +68,89 @@ func main() {
}
}

var createCommand = cli.Command{
Name: "create",
Usage: "Create a new AVD",
Category: categoryManage,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "system-image",
Usage: "Identifier of the system image to flash AVD with. Run 'sdkmanager --list_installed | grep system-images' to see what you have",
Required: true,
// ShellComplete: SystemImages()
},
&cli.StringFlag{
Name: "device",
Aliases: []string{"skin"},
Usage: "Name of the device frame to use",
Required: true,
// ShellComplete: ls $ANDROID_HOME/skins
},
&cli.IntFlag{
Name: "sdcard",
Usage: "Size of SD card",
Value: 4096,
// ShellComplete: common sizes (4096M, 8192M)
},
},
Action: func(ctx context.Context, c *cli.Command) error {
osImage := emulator.SystemImage(c.String("system-image"))
device := c.String("device")
sdcardSizeMB := int(c.Int("sdcard"))

arch := "x86_64"
if runtime.GOARCH == "arm64" {
arch = "arm64-v8a"
}
_ = arch

systemImages, err := emulator.SystemImages()
if err != nil {
return fmt.Errorf("get system images: %w", err)
}

validSystemImage := false
for _, systemImage := range systemImages {
if osImage == systemImage {
validSystemImage = true
break
}
}

if !validSystemImage {
return fmt.Errorf("could not find a OS image '%s'", osImage)
}

skins, err := emulator.Skins()
if err != nil {
return fmt.Errorf("get skins: %w", err)
}

validSkin := false
for _, skin := range skins {
if device == skin {
validSkin = true
break
}
}

if !validSkin {
return fmt.Errorf("could not find a valid skin '%s'", device)
}

avdName, avdPath, err := emulator.CreateAVD(osImage, device, sdcardSizeMB)
if err != nil {
return fmt.Errorf("create AVD: %w", err)
}

if emulator.PrintInvocations {
fmt.Printf("Created AVD '%s' in %s\n", avdName, avdPath)
}

return nil
},
}

var runCommand = cli.Command{
Name: "run",
Usage: "Boot AVD",
Expand Down
7 changes: 0 additions & 7 deletions emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package emulator
import (
"bytes"
"fmt"
"log"
"os/exec"
"slices"
"strconv"
Expand Down Expand Up @@ -290,9 +289,3 @@ func adbShell(cmd ...string) error {
}
return nil
}

func printInvocation(cmd *exec.Cmd) {
if PrintInvocations {
log.Println(cmd.String())
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ go 1.23
require (
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.0.0-alpha9.7
golang.org/x/text v0.21.0
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect

)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ github.com/urfave/cli/v3 v3.0.0-alpha9.7 h1:pkcHMTcfp821SiXRuYBkXdo30X8cuBzuJ95p
github.com/urfave/cli/v3 v3.0.0-alpha9.7/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61 changes: 61 additions & 0 deletions sdkmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package emulator

import (
"fmt"
"os/exec"
"strings"
)

// SystemImage is a unique identifier of an Android OS image .Examples:
// - system-images;android-34;google_apis_playstore;arm64-v8a
// - system-images;android-35;google_apis;x86_64
type SystemImage string

// ApiLevel returns the API level of this system image.
// Error can be returned when e.g. the new Android version still has only a codename, not a number.
func (s SystemImage) ApiLevel() string {
substrings := strings.Split(string(s), ";")
if len(substrings) != 4 {
panic("invalid output")
}

// E.g. android-35, android-Baklava, or android-34-ext9
androidPart := substrings[1]
substrings = strings.Split(androidPart, "-")
if len(substrings) == 0 {
panic("invalid output")
}

return substrings[1]
}

// SystemImages returns installed Android system images.
func SystemImages() ([]SystemImage, error) {
systemImages := make([]SystemImage, 0)

cmd := exec.Command("sdkmanager", "--list_installed")
printInvocation(cmd)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to run sdkmanager: %v", err)
}

// Sample output:
// system-images;android-33;google_apis;arm64-v8a | 17 | Google APIs ARM 64 v8a System Image | system-images/android-33/google_apis/arm64-v8a
// system-images;android-33;google_apis_playstore;arm64-v8a | 9 | Google Play ARM 64 v8a System Image | system-images/android-33/google_apis_playstore/arm64-v8a
lines := strings.Split(string(output), "\n")

for _, line := range lines {
line = strings.TrimSpace(line)
line := strings.Split(line, " ")[0]
if strings.HasPrefix(line, "system-images;") {
systemImages = append(systemImages, SystemImage(line))
}
}

for _, systemImage := range systemImages {
fmt.Printf("%s\n", systemImage)
}

return systemImages, nil
}
12 changes: 12 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package emulator

import (
"log"
"os/exec"
)

func printInvocation(cmd *exec.Cmd) {
if PrintInvocations {
log.Println(cmd.String())
}
}

0 comments on commit 080aa07

Please sign in to comment.