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"`},
+}