From af353822fa5e0f9e9f8416a662021e8c8f5ea001 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Fri, 1 Nov 2019 13:53:01 -0400 Subject: [PATCH] cmd/gogio: add permissions system for Android Search for imports of the form gioui.org/app/permission/* and add required permissions to AndroidManifest.xml. Signed-off-by: Greg Pomerantz --- app/doc.go | 36 ++++++++- app/permission/bluetooth/main.go | 1 + app/permission/bluetooth_le/main.go | 1 + cmd/gogio/androidbuild.go | 113 +++++++++++++++++++++++----- cmd/gogio/permission.go | 23 ++++++ 5 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 app/permission/bluetooth/main.go create mode 100644 app/permission/bluetooth_le/main.go create mode 100644 cmd/gogio/permission.go diff --git a/app/doc.go b/app/doc.go index db3e09e0..0000e158 100644 --- a/app/doc.go +++ b/app/doc.go @@ -36,7 +36,7 @@ DestroyEvent is received. Main -The Main function must be called from a programs main function, to hand over +The Main function must be called from a program's main function, to hand over control of the main thread to operating systems that need it. Because Main is also blocking, the event loop of a Window must run in a goroutine. @@ -63,5 +63,39 @@ A Window's Queue method returns an event.Queue implementation that distributes incoming events to the event handlers declared in the latest frame. See the gioui.org/ui package for more information about event handlers. +Permissions + +The packages under gioui.org/app/permission should be imported +by a Gio program or by one of its dependencies to indicate that specific +operating-system permissions are required. For example, if a Gio +program requires access to a device's Bluetooth interface, it +should import "gioui.org/app/permission/bluetooth" as follows: + + package main + + import ( + "gioui.org/app" + _ "gioui.org/app/permission/bluetooth" + ) + + func main() { + ... + } + +Since there are no exported identifiers in the app/permission/bluetooth +package, the import uses the anonymous identifier (_) as the imported +package name. + +As a special case, the gogio tool detects when a program directly or +indirectly depends on the "net" package from the Go standard library as an +indication that the program requires network access permissions. If a program +requires network permissions but does not directly or indirectly import +"net", it will be necessary to add the following code somewhere in the +program's source code: + + import ( + ... + _ "net" + ) */ package app diff --git a/app/permission/bluetooth/main.go b/app/permission/bluetooth/main.go new file mode 100644 index 00000000..b3b971b1 --- /dev/null +++ b/app/permission/bluetooth/main.go @@ -0,0 +1 @@ +package bluetooth diff --git a/app/permission/bluetooth_le/main.go b/app/permission/bluetooth_le/main.go new file mode 100644 index 00000000..6fcf9684 --- /dev/null +++ b/app/permission/bluetooth_le/main.go @@ -0,0 +1 @@ +package bluetooth_le diff --git a/cmd/gogio/androidbuild.go b/cmd/gogio/androidbuild.go index 4209ecf3..435e2637 100644 --- a/cmd/gogio/androidbuild.go +++ b/cmd/gogio/androidbuild.go @@ -4,6 +4,7 @@ package main import ( "archive/zip" + "bytes" "errors" "fmt" "io" @@ -15,6 +16,7 @@ import ( "runtime" "strconv" "strings" + "text/template" "golang.org/x/sync/errgroup" "golang.org/x/tools/go/packages" @@ -39,6 +41,17 @@ type errWriter struct { var exeSuffix string +type manifestData struct { + AppID string + Version int + MinSDK int + TargetSDK int + Permissions []string + Features []string + IconSnip string + AppName string +} + func init() { if runtime.GOOS == "windows" { exeSuffix = ".exe" @@ -67,6 +80,8 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { androidjar: filepath.Join(platform, "android.jar"), } + perms := []string{"default"} + const permPref = "gioui.org/app/permission/" cfg := &packages.Config{ Mode: packages.NeedName + packages.NeedFiles + @@ -95,6 +110,12 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { return err } extraJars = append(extraJars, jars...) + switch { + case p.PkgPath == "net": + perms = append(perms, "network") + case strings.HasPrefix(p.PkgPath, permPref): + perms = append(perms, p.PkgPath[len(permPref):]) + } for _, imp := range p.Imports { if !visitedPkgs[imp.ID] { @@ -113,9 +134,9 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { } switch *buildMode { case "archive": - return archiveAndroid(tmpDir, bi) + return archiveAndroid(tmpDir, bi, perms) case "exe": - if err := exeAndroid(tmpDir, tools, bi, extraJars); err != nil { + if err := exeAndroid(tmpDir, tools, bi, extraJars, perms); err != nil { return err } return signAPK(tmpDir, tools, bi) @@ -206,7 +227,7 @@ func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err erro return builds.Wait() } -func archiveAndroid(tmpDir string, bi *buildInfo) (err error) { +func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) { aarFile := *destPath if aarFile == "" { aarFile = fmt.Sprintf("%s.aar", bi.name) @@ -227,11 +248,25 @@ func archiveAndroid(tmpDir string, bi *buildInfo) (err error) { defer aarw.Close() aarw.Create("R.txt") aarw.Create("res/") + permissions, features := getPermissions(perms) manifest := aarw.Create("AndroidManifest.xml") - manifest.Write([]byte(fmt.Sprintf(` - - -`, bi.appID, bi.minsdk))) + manifestSrc := manifestData{ + AppID: bi.appID, + MinSDK: bi.minsdk, + Permissions: permissions, + Features: features, + } + tmpl, err := template.New("manifest").Parse( + ` + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} +`) + if err != nil { + panic(err) + } + err = tmpl.Execute(manifest, manifestSrc) proguard := aarw.Create("proguard.txt") proguard.Write([]byte(`-keep class org.gioui.** { *; }`)) @@ -251,7 +286,7 @@ func archiveAndroid(tmpDir string, bi *buildInfo) (err error) { return aarw.Close() } -func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars []string) (err error) { +func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string) (err error) { classes := filepath.Join(tmpDir, "classes") var classFiles []string err = filepath.Walk(classes, func(path string, f os.FileInfo, err error) error { @@ -342,18 +377,30 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars []s if bi.minsdk > targetSDK { targetSDK = bi.minsdk } + permissions, features := getPermissions(perms) appName := strings.Title(bi.name) - manifestSrc := fmt.Sprintf(` + manifestSrc := manifestData{ + AppID: bi.appID, + Version: bi.version, + MinSDK: bi.minsdk, + TargetSDK: targetSDK, + Permissions: permissions, + Features: features, + IconSnip: iconSnip, + AppName: appName, + } + tmpl, err := template.New("test").Parse( + ` - - - - + package="{{.AppID}}" + android:versionCode="{{.Version}}" + android:versionName="1.0.{{.Version}}"> + +{{range .Permissions}} +{{end}}{{range .Features}} +{{end}} @@ -363,11 +410,16 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars []s -`, bi.appID, bi.version, bi.version, bi.minsdk, targetSDK, iconSnip, appName, appName) - manifest := filepath.Join(tmpDir, "AndroidManifest.xml") - if err := ioutil.WriteFile(manifest, []byte(manifestSrc), 0660); err != nil { +`) + var manifestBuffer bytes.Buffer + if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil { return err } + manifest := filepath.Join(tmpDir, "AndroidManifest.xml") + if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(), 0660); err != nil { + return err + } + tmpapk := filepath.Join(tmpDir, "link.apk") link := exec.Command( aapt2, @@ -597,6 +649,27 @@ func archNDK() string { } } +func getPermissions(ps []string) ([]string, []string) { + var permissions, features []string + seenPermissions := make(map[string]bool) + seenFeatures := make(map[string]bool) + for _, perm := range ps { + for _, x := range AndroidPermissions[perm] { + if !seenPermissions[x] { + permissions = append(permissions, x) + seenPermissions[x] = true + } + } + for _, x := range AndroidFeatures[perm] { + if !seenFeatures[x] { + features = append(features, x) + seenFeatures[x] = true + } + } + } + return permissions, features +} + func latestPlatform(sdk string) (string, error) { allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*")) if err != nil { diff --git a/cmd/gogio/permission.go b/cmd/gogio/permission.go new file mode 100644 index 00000000..eabbb911 --- /dev/null +++ b/cmd/gogio/permission.go @@ -0,0 +1,23 @@ +package main + +var AndroidPermissions = map[string][]string{ + "network": { + "android.permission.INTERNET", + }, + "bluetooth": { + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.ACCESS_FINE_LOCATION", + }, + "bluetooth_le": { + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.ACCESS_FINE_LOCATION", + }, +} + +var AndroidFeatures = map[string][]string{ + "default": {`glEsVersion="0x00030000"`}, + "bluetooth": {`name="android.hardware.bluetooth"`}, + "bluetooth_le": {`name="android.hardware.bluetooth_le"`}, +}