├── internal ├── chanbuf │ └── chanbuf.go ├── debug │ └── stackonexit.go ├── io │ ├── interface.go │ └── copyTimeout.go ├── unix │ └── socket.go ├── api │ └── termuxapi.go └── intent │ └── broadcast.go ├── dialog.go ├── fingerprint.go ├── torch.go ├── error.go ├── download.go ├── package.go ├── viberate.go ├── clipboard.go ├── cameraPhoto.go ├── contactList.go ├── brightness.go ├── batteryStatus.go ├── callLog.go ├── wallpaper.go ├── audioInfo.go ├── volume.go ├── cameraInfo.go ├── toast.go ├── README.md ├── sms.go ├── sensor.go ├── share.go ├── wifi.go ├── exec.go ├── notification.go ├── location.go └── LICENSE /internal/chanbuf/chanbuf.go: -------------------------------------------------------------------------------- 1 | package chanbuf 2 | 3 | type BufToChan struct { 4 | C chan<- []byte 5 | } 6 | 7 | func (c BufToChan) Write(p []byte) (int, error) { 8 | c.C <- p 9 | return len(p), nil 10 | } 11 | -------------------------------------------------------------------------------- /dialog.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "errors" 4 | 5 | // Dialog pops up a dialog on the device 6 | // Not implemented yet as the original api does not work on mmy device 7 | func Dialog() error { 8 | return errors.New("not implemented") 9 | } 10 | -------------------------------------------------------------------------------- /fingerprint.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "errors" 4 | 5 | // Fingerprint uses the fingerprint sensor on the device for authentication 6 | // Not implemented as the original API does not work on my device 7 | func Fingerprint() error { 8 | return errors.New("not implemented") 9 | } 10 | -------------------------------------------------------------------------------- /internal/debug/stackonexit.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "runtime/pprof" 7 | "syscall" 8 | ) 9 | 10 | func StackOnExit() { 11 | c := make(chan os.Signal) 12 | signal.Notify(c, syscall.SIGINT) 13 | go func() { 14 | <-c 15 | pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) 16 | os.Exit(0) 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /torch.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "bytes" 4 | 5 | // TorchEnable sets the current state of the device flashlight 6 | func TorchEnable(enabled bool) error { 7 | buf := bytes.NewBuffer([]byte{}) 8 | if err := exec(nil, buf, "Torch", map[string]interface{}{ 9 | "enabled": enabled, 10 | }, ""); err != nil { 11 | return err 12 | } 13 | return checkErr(buf.Bytes()) 14 | } 15 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | func checkErr(data []byte) error { 9 | x := struct { 10 | Error *string `json:"error"` 11 | APIError *string `json:"API_ERROR"` 12 | }{} 13 | json.Unmarshal(data, &x) 14 | if x.Error != nil { 15 | return errors.New(*(x.Error)) 16 | } 17 | if x.APIError != nil { 18 | return errors.New(*(x.APIError)) 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "bytes" 4 | 5 | // Download calls the system download manager to download an URL 6 | func Download(desc string, title string, url string) error { 7 | buf := bytes.NewBuffer([]byte{}) 8 | if err := exec(nil, buf, "Download", map[string]interface{}{ 9 | "title": title, 10 | "description": desc, 11 | }, url); err != nil { 12 | return err 13 | } 14 | return checkErr(buf.Bytes()) 15 | } 16 | -------------------------------------------------------------------------------- /internal/io/interface.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | _io "io" 5 | "time" 6 | ) 7 | 8 | type CloseReader interface { 9 | _io.Reader 10 | CloseRead() error 11 | } 12 | type CloseWriter interface { 13 | _io.Writer 14 | CloseWrite() error 15 | } 16 | type DeadlineReader interface { 17 | _io.Reader 18 | SetReadDeadline(t time.Time) error 19 | } 20 | type DeadlineWriter interface { 21 | _io.Reader 22 | SetWriteDeadline(t time.Time) error 23 | } 24 | -------------------------------------------------------------------------------- /package.go: -------------------------------------------------------------------------------- 1 | // Package termux implements termux:API calls and wraps them into golang function calls. 2 | /* 3 | Example: 4 | package main 5 | 6 | import tm "github.com/eternal-flame-AD/go-termux" 7 | 8 | func main() { 9 | if err := tm.ClipboardSet("ummmm"); err != nil { 10 | panic(err) 11 | } 12 | } 13 | 14 | 15 | We call termux:API methods directly so that this package would work without termux-api package installed. 16 | */ 17 | package termux 18 | -------------------------------------------------------------------------------- /viberate.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "bytes" 4 | 5 | // Viberate creates a viberation on the device, force means whether to viberate even if the device is set to silent mode 6 | func Viberate(ms int, force bool) error { 7 | buf := bytes.NewBuffer([]byte{}) 8 | if err := exec(nil, buf, "Viberate", map[string]interface{}{ 9 | "duration_ms": ms, 10 | "force": force, 11 | }, ""); err != nil { 12 | return err 13 | } 14 | return checkErr(buf.Bytes()) 15 | } 16 | -------------------------------------------------------------------------------- /clipboard.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // ClipboardGet gets the current content of the clipboard 8 | func ClipboardGet() (string, error) { 9 | buf := bytes.NewBuffer([]byte{}) 10 | if err := exec(nil, buf, "Clipboard", nil, ""); err != nil { 11 | return "", err 12 | } 13 | return buf.String(), nil 14 | } 15 | 16 | // ClipboardSet sets the clipboard to the given value 17 | func ClipboardSet(val string) error { 18 | inbuf := bytes.NewBufferString(val) 19 | buf := bytes.NewBuffer([]byte{}) 20 | exec(inbuf, buf, "Clipboard", map[string]interface{}{ 21 | "set": true, 22 | "api_version": "2", 23 | }, "") 24 | return checkErr(buf.Bytes()) 25 | } 26 | -------------------------------------------------------------------------------- /cameraPhoto.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path" 7 | ) 8 | 9 | // TakePhoto uses the device camera to take a photo and save the result to outfile 10 | // cameraID is the id of the camera specified by the return of CameraInfo 11 | func TakePhoto(cameraID string, outfile string) error { 12 | buf := bytes.NewBuffer([]byte{}) 13 | if !path.IsAbs(outfile) { 14 | wd, err := os.Getwd() 15 | if err != nil { 16 | return err 17 | } 18 | outfile = path.Join(wd, outfile) 19 | } 20 | if err := exec(nil, buf, "CameraPhoto", map[string]interface{}{ 21 | "camera": cameraID, 22 | "file": outfile, 23 | }, ""); err != nil { 24 | return err 25 | } 26 | res := buf.Bytes() 27 | return checkErr(res) 28 | } 29 | -------------------------------------------------------------------------------- /contactList.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // ContactPiece represents one contact info 9 | type ContactPiece struct { 10 | Name string `json:"name"` 11 | Number string `json:"number"` 12 | } 13 | 14 | // ContactList gets all contact from the device 15 | func ContactList() ([]ContactPiece, error) { 16 | buf := bytes.NewBuffer([]byte{}) 17 | if err := exec(nil, buf, "ContactList", nil, ""); err != nil { 18 | return nil, err 19 | } 20 | res := buf.Bytes() 21 | if err := checkErr(res); err != nil { 22 | return nil, err 23 | } 24 | records := make([]ContactPiece, 0) 25 | if err := json.Unmarshal(res, &records); err != nil { 26 | return nil, err 27 | } 28 | return records, nil 29 | } 30 | -------------------------------------------------------------------------------- /brightness.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // Brightness sets the current brightness level from 0 to 255 8 | func Brightness(level uint8) error { 9 | buf := bytes.NewBuffer([]byte{}) 10 | if err := exec(nil, buf, "Brightness", map[string]interface{}{ 11 | "brightness": int(level), 12 | "auto": false, 13 | }, ""); err != nil { 14 | return err 15 | } 16 | res := buf.Bytes() 17 | return checkErr(res) 18 | } 19 | 20 | // BrightnessAuto sets the current brightness to auto 21 | func BrightnessAuto() error { 22 | buf := bytes.NewBuffer([]byte{}) 23 | if err := exec(nil, buf, "Brightness", map[string]interface{}{ 24 | "auto": true, 25 | }, ""); err != nil { 26 | return err 27 | } 28 | res := buf.Bytes() 29 | return checkErr(res) 30 | } 31 | -------------------------------------------------------------------------------- /internal/io/copyTimeout.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | _io "io" 5 | "time" 6 | ) 7 | 8 | func TrySetReadDeadline(t time.Time, s _io.Reader) error { 9 | if sd, ok := s.(DeadlineReader); ok { 10 | if err := sd.SetReadDeadline(t); err != nil { 11 | return err 12 | } 13 | } 14 | return nil 15 | } 16 | 17 | func TrySetWriteDeadline(t time.Time, s _io.Writer) error { 18 | if sd, ok := s.(DeadlineWriter); ok { 19 | if err := sd.SetWriteDeadline(t); err != nil { 20 | return err 21 | } 22 | } 23 | return nil 24 | } 25 | 26 | func CopyDeadline(time time.Time, dst _io.Writer, src _io.Reader) (int64, error) { 27 | if err := TrySetReadDeadline(time, src); err != nil { 28 | return 0, err 29 | } 30 | if err := TrySetWriteDeadline(time, dst); err != nil { 31 | return 0, err 32 | } 33 | return _io.Copy(dst, src) 34 | } 35 | -------------------------------------------------------------------------------- /batteryStatus.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // BatteryStatusResponse represents the current battery status 9 | type BatteryStatusResponse struct { 10 | Health string `json:"health"` 11 | Percentage int `json:"percentage"` 12 | Plugged string `json:"plugged"` 13 | Status string `json:"status"` 14 | Temperature float64 `json:"temperature"` 15 | } 16 | 17 | // BatteryStatus acquires the corrent audio info 18 | func BatteryStatus() (*BatteryStatusResponse, error) { 19 | buf := bytes.NewBuffer([]byte{}) 20 | if err := exec(nil, buf, "BatteryStatus", nil, ""); err != nil { 21 | return nil, err 22 | } 23 | res := buf.Bytes() 24 | if err := checkErr(res); err != nil { 25 | return nil, err 26 | } 27 | ret := new(BatteryStatusResponse) 28 | if err := json.Unmarshal(res, ret); err != nil { 29 | return nil, err 30 | } 31 | return ret, nil 32 | } 33 | -------------------------------------------------------------------------------- /callLog.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // CallLogPiece represents one piece of call log 9 | type CallLogPiece struct { 10 | Name string `json:"name"` 11 | Number string `json:"phone_number"` 12 | Type string `json:"type"` 13 | Date string `json:"date"` 14 | Duration string `json:"duration"` 15 | } 16 | 17 | // CallLog acquires call logs with a given limit and offset 18 | func CallLog(limit int, offset int) ([]CallLogPiece, error) { 19 | buf := bytes.NewBuffer([]byte{}) 20 | if err := exec(nil, buf, "CallLog", map[string]interface{}{ 21 | "limit": limit, 22 | "offset": offset, 23 | }, ""); err != nil { 24 | return nil, err 25 | } 26 | res := buf.Bytes() 27 | if err := checkErr(res); err != nil { 28 | return nil, err 29 | } 30 | records := make([]CallLogPiece, 0) 31 | if err := json.Unmarshal(res, &records); err != nil { 32 | return nil, err 33 | } 34 | return records, nil 35 | } 36 | -------------------------------------------------------------------------------- /wallpaper.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "path/filepath" 6 | ) 7 | 8 | // WallpaperFile sets the current wallpaper, main screen if lockscreen is false 9 | func WallpaperFile(path string, lockscreen bool) error { 10 | realpath, err := filepath.Abs(path) 11 | if err != nil { 12 | return err 13 | } 14 | buf := bytes.NewBuffer([]byte{}) 15 | if err := exec(nil, buf, "Wallpaper", map[string]interface{}{ 16 | "file": realpath, 17 | "lockscreen": lockscreen, 18 | }, ""); err != nil { 19 | return err 20 | } 21 | return checkErr(buf.Bytes()) 22 | } 23 | 24 | // WallpaperURL sets the current wallpaper, main screen if lockscreen is false 25 | func WallpaperURL(url string, lockscreen bool) error { 26 | buf := bytes.NewBuffer([]byte{}) 27 | if err := exec(nil, buf, "Wallpaper", map[string]interface{}{ 28 | "url": url, 29 | "lockscreen": lockscreen, 30 | }, ""); err != nil { 31 | return err 32 | } 33 | return checkErr(buf.Bytes()) 34 | } 35 | -------------------------------------------------------------------------------- /audioInfo.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // AudioInfoResponse represents the current audio stream status 9 | type AudioInfoResponse struct { 10 | JavaOutputSampleRate string `json:"PROPERTY_OUTPUT_SAMPLE_RATE"` 11 | JavaOutputFramesPerBuffer string `json:"PROPERTY_OUTPUT_FRAMES_PER_BUFFER"` 12 | AudioTrackOutputSampleRate int `json:"AUDIOTRACK_NATIVE_OUTPUT_SAMPLE_RATE"` 13 | BluetoothA2DP bool `json:"BLUETOOTH_A2DP_IS_ON"` 14 | WiredHeadsetConnected bool `json:"WIREDHEADSET_IS_CONNECTED"` 15 | } 16 | 17 | // AudioInfo acquires the corrent audio info 18 | func AudioInfo() (*AudioInfoResponse, error) { 19 | buf := bytes.NewBuffer([]byte{}) 20 | if err := exec(nil, buf, "AudioInfo", nil, ""); err != nil { 21 | return nil, err 22 | } 23 | res := buf.Bytes() 24 | if err := checkErr(res); err != nil { 25 | return nil, err 26 | } 27 | ret := new(AudioInfoResponse) 28 | if err := json.Unmarshal(res, ret); err != nil { 29 | return nil, err 30 | } 31 | return ret, nil 32 | } 33 | -------------------------------------------------------------------------------- /volume.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // AudioStreamState represents the volume info of an audio stream 9 | type AudioStreamState struct { 10 | Name string `json:"stream"` 11 | Volume int `json:"volume"` 12 | MaxVolume int `json:"max_volume"` 13 | } 14 | 15 | // AudioStreams acquires all audio stream volume info from the device 16 | func AudioStreams() ([]AudioStreamState, error) { 17 | buf := bytes.NewBuffer([]byte{}) 18 | if err := exec(nil, buf, "Volume", nil, ""); err != nil { 19 | return nil, err 20 | } 21 | res := buf.Bytes() 22 | 23 | if err := checkErr(res); res != nil { 24 | return nil, err 25 | } 26 | l := make([]AudioStreamState, 0) 27 | if err := json.Unmarshal(res, l); err != nil { 28 | return nil, err 29 | } 30 | return l, nil 31 | } 32 | 33 | // AudioStreamVolume sets the volume of a given audio stream name 34 | func AudioStreamVolume(name string, volume int) error { 35 | buf := bytes.NewBuffer([]byte{}) 36 | if err := exec(nil, buf, "Volume", map[string]interface{}{ 37 | "stream": name, 38 | "volume": volume, 39 | }, ""); err != nil { 40 | return err 41 | } 42 | return checkErr(buf.Bytes()) 43 | } 44 | -------------------------------------------------------------------------------- /cameraInfo.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // CameraInfoPiece represent the info of one camera on the device 9 | type CameraInfoPiece struct { 10 | ID string `json:"id"` 11 | Facing string `json:"facing"` 12 | JPEGOutputSizes []struct { 13 | Width int `json:"width"` 14 | Height int `json:"height"` 15 | } `json:"jpeg_output_sizes"` 16 | FocalLengths []float64 `json:"focal_lengths"` 17 | AEModes []string `json:"auto_exposure_modes"` 18 | PhysicalSize struct { 19 | Width float64 `json:"width"` 20 | Height float64 `json:"height"` 21 | } `json:"physical_size"` 22 | Capabilities []string `json:"capabilities"` 23 | } 24 | 25 | // CameraInfo gets the information of available cameras on the device 26 | func CameraInfo() ([]CameraInfoPiece, error) { 27 | buf := bytes.NewBuffer([]byte{}) 28 | if err := exec(nil, buf, "CameraInfo", nil, ""); err != nil { 29 | return nil, err 30 | } 31 | res := buf.Bytes() 32 | if err := checkErr(res); err != nil { 33 | return nil, err 34 | } 35 | records := make([]CameraInfoPiece, 0) 36 | if err := json.Unmarshal(res, &records); err != nil { 37 | return nil, err 38 | } 39 | return records, nil 40 | } 41 | -------------------------------------------------------------------------------- /toast.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "bytes" 4 | 5 | // ToastPosition enumerates current position of the toast 6 | type ToastPosition string 7 | 8 | const ( 9 | // Top lifts the toast ot the top of the screen 10 | Top ToastPosition = "top" 11 | // Middle is the default position of the toast 12 | Middle ToastPosition = "middle" 13 | // Bottom puts the toast to the bottom of the screen 14 | Bottom ToastPosition = "bottom" 15 | ) 16 | 17 | // ToastOption represents the optional options to a toast 18 | type ToastOption struct { 19 | BGColor string 20 | FontColor string 21 | Position ToastPosition 22 | Short bool 23 | } 24 | 25 | // Toast creates a toast on the device 26 | func Toast(text string, opts ToastOption) error { 27 | in := bytes.NewBufferString(text) 28 | buf := bytes.NewBuffer([]byte{}) 29 | params := map[string]interface{}{} 30 | if opts.FontColor != "" { 31 | params["text_color"] = opts.FontColor 32 | } 33 | if opts.BGColor != "" { 34 | params["background"] = opts.BGColor 35 | } 36 | if opts.Short { 37 | params["short"] = true 38 | } 39 | if opts.Position != "" { 40 | params["gravity"] = string(opts.Position) 41 | } 42 | if err := exec(in, buf, "Toast", params, ""); err != nil { 43 | return err 44 | } 45 | return checkErr(buf.Bytes()) 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-termux 2 | 3 | [![API Documentation](https://img.shields.io/badge/api-GoDoc-blue.svg?style=flat-square)](https://godoc.org/github.com/eternal-flame-AD/go-termux) 4 | 5 | Golang wrapper for termux:API. This package calls termux:API methods directly so that this package would work without termux-api package installed. 6 | 7 | ## Examples 8 | 9 | ### Acquires the current battery percentage 10 | 11 | ```golang 12 | package main 13 | 14 | import ( 15 | "fmt" 16 | 17 | tm "github.com/eternal-flame-AD/go-termux" 18 | ) 19 | 20 | func main() { 21 | if stat, err := tm.BatteryStatus(); err != nil { 22 | panic(err) 23 | } else { 24 | fmt.Printf("The current battery percentage is %d%%.\n", stat.Percentage) 25 | } 26 | } 27 | ``` 28 | 29 | ### Sets current clipboard content 30 | ```golang 31 | package main 32 | 33 | import tm "github.com/eternal-flame-AD/go-termux" 34 | 35 | func main() { 36 | if err := tm.ClipboardSet("ummmm"); err != nil { 37 | panic(err) 38 | } 39 | } 40 | ``` 41 | 42 | ### Displays a short toast 43 | ```golang 44 | package main 45 | 46 | import ( 47 | tm "github.com/eternal-flame-AD/go-termux" 48 | ) 49 | 50 | func main() { 51 | if err := tm.Toast("Hello World!", tm.ToastOption{ 52 | FontColor: "#FF0000", 53 | Position: tm.Top, 54 | Short: true, 55 | BGColor: "#00FF00", 56 | }); err != nil { 57 | panic(err) 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /sms.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // SMSBoxType enumberates available sms box types 9 | type SMSBoxType int 10 | 11 | const ( 12 | // All sms box type 13 | All SMSBoxType = 0 14 | // Inbox sms box type 15 | Inbox SMSBoxType = 1 16 | // Sent sms box type 17 | Sent SMSBoxType = 2 18 | // Draft sms box type 19 | Draft SMSBoxType = 3 20 | // Outbox sms box type 21 | Outbox SMSBoxType = 4 22 | ) 23 | 24 | // SMS represents a piece of received SMS 25 | type SMS struct { 26 | ThreadID int `json:"threadid"` 27 | Type string `json:"type"` 28 | Read bool `json:"read"` 29 | Sender string `json:"sender"` 30 | Number string `json:"number"` 31 | Received string `json:"received"` 32 | Body string `json:"body"` 33 | } 34 | 35 | // SMSList acquires a list of the received SMS in the given SMS box with a given limit and offset 36 | func SMSList(limit int, offset int, box SMSBoxType) ([]SMS, error) { 37 | buf := bytes.NewBuffer([]byte{}) 38 | if err := exec(nil, buf, "SmsInbox", map[string]interface{}{ 39 | "type": box, 40 | "limit": limit, 41 | "offset": offset, 42 | }, ""); err != nil { 43 | return nil, err 44 | } 45 | res := buf.Bytes() 46 | if err := checkErr(res); err != nil { 47 | return nil, err 48 | } 49 | l := make([]SMS, 0) 50 | if err := json.Unmarshal(res, l); err != nil { 51 | return nil, err 52 | } 53 | return l, nil 54 | } 55 | 56 | // SMSSend sends a text message to the given recipient numbers 57 | func SMSSend(numbers []string, text string) error { 58 | in := bytes.NewBufferString(text) 59 | buf := bytes.NewBuffer([]byte{}) 60 | if err := exec(in, buf, "SmsSend", map[string]interface{}{ 61 | "recipients": numbers, 62 | }, ""); err != nil { 63 | return err 64 | } 65 | return checkErr(buf.Bytes()) 66 | } 67 | -------------------------------------------------------------------------------- /sensor.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "strings" 8 | 9 | "github.com/eternal-flame-AD/go-termux/internal/chanbuf" 10 | ) 11 | 12 | // SensorList acquires a list of available sensors on the device 13 | func SensorList() ([]string, error) { 14 | buf := bytes.NewBuffer([]byte{}) 15 | if err := execAction("Sensor", nil, buf, "list"); err != nil { 16 | return nil, err 17 | } 18 | res := buf.Bytes() 19 | 20 | if err := checkErr(res); res != nil { 21 | return nil, err 22 | } 23 | l := new(struct { 24 | Sensors []string `json:"sensors"` 25 | }) 26 | if err := json.Unmarshal(res, l); err != nil { 27 | return nil, err 28 | } 29 | return l.Sensors, nil 30 | } 31 | 32 | // SensorWatchOpt represents the options to a Sensor call 33 | type SensorWatchOpt struct { 34 | Limit int 35 | DelayMS int 36 | SensorList []string 37 | } 38 | 39 | // Sensor starts a sensor watch in a given context and options 40 | // returns raw data bytes encooded with JSON 41 | func Sensor(ctx context.Context, opt SensorWatchOpt) (<-chan []byte, error) { 42 | response := make(chan []byte) 43 | param := map[string]interface{}{} 44 | if opt.SensorList == nil { 45 | param["all"] = true 46 | } else { 47 | param["sensors"] = strings.Join(opt.SensorList, ",") 48 | } 49 | if opt.DelayMS != 0 { 50 | param["dalay"] = opt.DelayMS 51 | } 52 | if opt.Limit != 0 { 53 | param["limit"] = opt.Limit 54 | } 55 | if err := execContext(ctx, nil, chanbuf.BufToChan{ 56 | C: response, 57 | }, "Sensor", param, ""); err != nil { 58 | return nil, err 59 | } 60 | 61 | go func() { 62 | defer execAction("Sensor", nil, bytes.NewBuffer([]byte{}), "cleanup") 63 | for { 64 | select { 65 | case <-ctx.Done(): 66 | return 67 | } 68 | } 69 | }() 70 | 71 | return response, nil 72 | } 73 | -------------------------------------------------------------------------------- /share.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "path/filepath" 6 | ) 7 | 8 | // ShareType represent the action of the share, defaults to edit 9 | type ShareType = string 10 | 11 | const ( 12 | // View share type 13 | View ShareType = "view" 14 | // Edit share type 15 | Edit ShareType = "edit" 16 | // Send share type 17 | Send ShareType = "send" 18 | ) 19 | 20 | func shareParam(title string, useDefault bool, actionType ShareType) map[string]interface{} { 21 | if actionType == "" { 22 | actionType = "edit" 23 | } 24 | return map[string]interface{}{ 25 | "title": title, 26 | "default-receiver": useDefault, 27 | "action-type": string(actionType), 28 | } 29 | } 30 | 31 | // ShareFile shares a file with MIME type determined by the file extension, useDefault determines whether to use the default app if availables 32 | func ShareFile(title string, path string, useDefault bool, actionType ShareType) error { 33 | param := shareParam(title, useDefault, actionType) 34 | buf := bytes.NewBuffer([]byte{}) 35 | 36 | realpath, err := filepath.Abs(path) 37 | if err != nil { 38 | return err 39 | } 40 | param["file"] = realpath 41 | 42 | if err := exec(nil, buf, "Share", param, ""); err != nil { 43 | return err 44 | } 45 | return checkErr(buf.Bytes()) 46 | } 47 | 48 | // Share shares raw data bytes with the given content type, useDefault determines whether to use the default app if availables 49 | func Share(title string, data []byte, contentType string, useDefault bool, actionType ShareType) error { 50 | if contentType == "" { 51 | contentType = "text/plain" 52 | } 53 | param := shareParam(title, useDefault, actionType) 54 | param["content-type"] = contentType 55 | 56 | in := bytes.NewBuffer(data) 57 | buf := bytes.NewBuffer([]byte{}) 58 | if err := exec(in, buf, "Share", param, ""); err != nil { 59 | return err 60 | } 61 | return checkErr(buf.Bytes()) 62 | } 63 | -------------------------------------------------------------------------------- /internal/unix/socket.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | 8 | "github.com/twinj/uuid" 9 | ) 10 | 11 | func NewSocket() Socket { 12 | sock := getUnixSocket() 13 | l, err := net.Listen("unix", sock.Name) 14 | if err != nil { 15 | panic(err) 16 | } 17 | return Socket{ 18 | underlyingAddr: sock, 19 | underlyingListener: l, 20 | } 21 | } 22 | 23 | type Socket struct { 24 | underlyingAddr net.UnixAddr 25 | underlyingListener net.Listener 26 | underlyingConn *net.UnixConn 27 | } 28 | 29 | func (c *Socket) Name() string { 30 | return c.underlyingAddr.Name 31 | } 32 | 33 | func (c *Socket) connect() { 34 | conn, err := c.underlyingListener.Accept() 35 | if err != nil { 36 | panic(err) 37 | } 38 | c.underlyingConn = conn.(*net.UnixConn) 39 | } 40 | 41 | func (c *Socket) connectDeadline(t time.Time) error { 42 | timeout := time.NewTimer(t.Sub(time.Now())) 43 | done := make(chan struct{}) 44 | go func() { 45 | c.connect() 46 | close(done) 47 | }() 48 | defer timeout.Stop() 49 | select { 50 | case <-timeout.C: 51 | return errors.New("Connect timeout") 52 | case <-done: 53 | return nil 54 | } 55 | } 56 | 57 | func (c *Socket) SetReadDeadline(t time.Time) error { 58 | if c.underlyingConn == nil { 59 | if err := c.connectDeadline(t); err != nil { 60 | return err 61 | } 62 | } 63 | return c.underlyingConn.SetReadDeadline(t) 64 | } 65 | 66 | func (c *Socket) SetWriteDeadline(t time.Time) error { 67 | if c.underlyingConn == nil { 68 | if err := c.connectDeadline(t); err != nil { 69 | return err 70 | } 71 | } 72 | return c.underlyingConn.SetWriteDeadline(t) 73 | } 74 | 75 | func (c *Socket) Read(p []byte) (n int, err error) { 76 | if c.underlyingConn == nil { 77 | c.connect() 78 | } 79 | return c.underlyingConn.Read(p) 80 | } 81 | 82 | func (c *Socket) Write(p []byte) (n int, err error) { 83 | if c.underlyingConn == nil { 84 | c.connect() 85 | } 86 | return c.underlyingConn.Write(p) 87 | } 88 | 89 | func (c *Socket) Close() error { 90 | if c.underlyingConn == nil { 91 | return nil 92 | } 93 | return c.underlyingConn.Close() 94 | } 95 | 96 | func getUnixSocket() net.UnixAddr { 97 | return net.UnixAddr{Name: "@" + uuid.NewV4().String(), Net: "unix"} 98 | } 99 | -------------------------------------------------------------------------------- /internal/api/termuxapi.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | "github.com/eternal-flame-AD/go-termux/internal/intent" 9 | 10 | "github.com/eternal-flame-AD/go-termux/internal/unix" 11 | ) 12 | 13 | const termuxApiComponent = "com.termux.api/.TermuxApiReceiver" 14 | 15 | type Call struct { 16 | Method string 17 | Args map[string]interface{} 18 | Action string 19 | Data string 20 | pipeToRemote *unix.Socket 21 | pipeToMe *unix.Socket 22 | } 23 | 24 | func (c *Call) Call(ctx context.Context) { 25 | p1, p2 := unix.NewSocket(), unix.NewSocket() 26 | c.pipeToMe, c.pipeToRemote = &p1, &p2 27 | 28 | bc := intent.Broadcast{ 29 | User: 0, 30 | Component: termuxApiComponent, 31 | Data: c.Data, 32 | Action: c.Action, 33 | } 34 | bc.AddString("api_method", c.Method) 35 | bc.AddString("socket_input", strings.Trim(c.pipeToRemote.Name(), "@")) 36 | bc.AddString("socket_output", strings.Trim(c.pipeToMe.Name(), "@")) 37 | if c.Args != nil { 38 | for key, val := range c.Args { 39 | switch val.(type) { 40 | case string: 41 | bc.AddString(key, val.(string)) 42 | case bool: 43 | bc.AddBool(key, val.(bool)) 44 | case int: 45 | bc.AddInt(key, val.(int)) 46 | case []int32: 47 | bc.AddLongs(key, val.([]int32)) 48 | case []string: 49 | bc.AddStrings(key, val.([]string)) 50 | default: 51 | panic("Unsupported arg type") 52 | } 53 | } 54 | } 55 | bc.Send(ctx) 56 | } 57 | 58 | func (c Call) SetReadDeadline(t time.Time) error { 59 | return c.pipeToMe.SetReadDeadline(t) 60 | } 61 | 62 | func (c Call) Read(p []byte) (n int, err error) { 63 | return c.pipeToMe.Read(p) 64 | } 65 | 66 | func (c Call) SetWriteDeadline(t time.Time) error { 67 | return c.pipeToRemote.SetWriteDeadline(t) 68 | } 69 | 70 | func (c Call) Write(p []byte) (n int, err error) { 71 | return c.pipeToRemote.Write(p) 72 | } 73 | 74 | func (c Call) Close() error { 75 | if err := c.CloseRead(); err != nil { 76 | return err 77 | } 78 | if err := c.CloseWrite(); err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | 84 | func (c Call) CloseRead() error { 85 | return c.pipeToMe.Close() 86 | } 87 | 88 | func (c Call) CloseWrite() error { 89 | return c.pipeToRemote.Close() 90 | } 91 | -------------------------------------------------------------------------------- /wifi.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // WifiEnable sets the current enabled state of the device wifi 9 | func WifiEnable(enabled bool) error { 10 | buf := bytes.NewBuffer([]byte{}) 11 | exec(nil, buf, "WifiEnable", map[string]interface{}{ 12 | "enabled": enabled, 13 | }, "") 14 | res := buf.Bytes() 15 | return checkErr(res) 16 | } 17 | 18 | // WifiConnection represents the current connection info of the device wifi interface 19 | type WifiConnection struct { 20 | BSSID string `json:"bssid"` 21 | FreqMHZ int `json:"frequency_mhz"` 22 | IP string `json:"ip"` 23 | LinkSpeedMbps int `json:"link_speed_mbps"` 24 | MACAddr string `json:"mac_address"` 25 | NetworkID int `json:"network_id"` 26 | RSSI int `json:"rssi"` 27 | SSID string `json:"ssid"` 28 | SSIDHidden bool `json:"ssid_hidden"` 29 | SupplicantState string `json:"supplicant_state"` 30 | } 31 | 32 | // WifiConnectionState returns the current wifi connection state of the device 33 | func WifiConnectionState() (*WifiConnection, error) { 34 | buf := bytes.NewBuffer([]byte{}) 35 | if err := exec(nil, buf, "WifiConnectionInfo", nil, ""); err != nil { 36 | return nil, err 37 | } 38 | res := buf.Bytes() 39 | if err := checkErr(res); err != nil { 40 | return nil, err 41 | } 42 | ret := new(WifiConnection) 43 | if err := json.Unmarshal(res, ret); err != nil { 44 | return nil, err 45 | } 46 | return ret, nil 47 | } 48 | 49 | // WifiAP represents a discovered AP in a wifi scan 50 | type WifiAP struct { 51 | BSSID string `json:"bssid"` 52 | FreqMHZ int `json:"frequency_mhz"` 53 | RSSI int `json:"rssi"` 54 | SSID string `json:"ssid"` 55 | TimeStamp int64 `json:"timestamp"` 56 | // CenterFreqMHZ not used for 20Mhz bands 57 | CenterFreqMHZ int `json:"center_frequency_mhz"` 58 | ChannelBandwidth string `json:"channel_bandwidth_mhz"` 59 | } 60 | 61 | // WifiScan scans for available networks 62 | func WifiScan() ([]WifiAP, error) { 63 | buf := bytes.NewBuffer([]byte{}) 64 | execAction("WifiScanInfo", nil, buf, "list") 65 | res := buf.Bytes() 66 | 67 | if err := checkErr(res); res != nil { 68 | return nil, err 69 | } 70 | l := make([]WifiAP, 0) 71 | if err := json.Unmarshal(res, l); err != nil { 72 | return nil, err 73 | } 74 | return l, nil 75 | } 76 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/eternal-flame-AD/go-termux/internal/api" 10 | _io "github.com/eternal-flame-AD/go-termux/internal/io" 11 | ) 12 | 13 | // GlobalTimeout The global timeout to non-persistent and non-time-consumingn operations 14 | var GlobalTimeout = 15 * time.Second 15 | 16 | func tryCloseReader(c io.Reader) { 17 | if r, ok := c.(_io.CloseReader); ok { 18 | r.CloseRead() 19 | } 20 | } 21 | func tryCloseWriter(c io.Writer) { 22 | if r, ok := c.(_io.CloseWriter); ok { 23 | r.CloseWrite() 24 | } 25 | } 26 | 27 | func pipe(ctx context.Context, ai io.Reader, ao io.Writer, bi io.Reader, bo io.Writer) (err error) { 28 | wg := sync.WaitGroup{} 29 | 30 | if bi != nil && ao != nil { 31 | wg.Add(1) 32 | go func() { 33 | if t, ok := ctx.Deadline(); ok { 34 | _, err = _io.CopyDeadline(t, ao, bi) 35 | } else { 36 | _, err = io.Copy(ao, bi) 37 | } 38 | tryCloseReader(bi) 39 | tryCloseWriter(ao) 40 | wg.Done() 41 | }() 42 | } 43 | if ai != nil && bo != nil { 44 | wg.Add(1) 45 | go func() { 46 | if t, ok := ctx.Deadline(); ok { 47 | _, err = _io.CopyDeadline(t, bo, ai) 48 | } else { 49 | _, err = io.Copy(bo, ai) 50 | } 51 | tryCloseReader(ai) 52 | tryCloseWriter(bo) 53 | wg.Done() 54 | }() 55 | } 56 | wg.Wait() 57 | return 58 | } 59 | 60 | func execAction(method string, stdin io.Reader, stdout io.Writer, action string) error { 61 | ctx, cancel := context.WithTimeout(context.Background(), GlobalTimeout) 62 | defer cancel() 63 | return execActionContext(ctx, stdin, stdout, method, action) 64 | } 65 | 66 | func execActionContext(ctx context.Context, stdin io.Reader, stdout io.Writer, method string, action string) error { 67 | call := api.Call{ 68 | Method: method, 69 | Action: action, 70 | } 71 | 72 | call.Call(ctx) 73 | return pipe(ctx, call, call, stdin, stdout) 74 | } 75 | 76 | func exec(stdin io.Reader, stdout io.Writer, method string, args map[string]interface{}, data string) error { 77 | ctx, cancel := context.WithTimeout(context.Background(), GlobalTimeout) 78 | defer cancel() 79 | return execContext(ctx, stdin, stdout, method, args, data) 80 | } 81 | 82 | func execContext(ctx context.Context, stdin io.Reader, stdout io.Writer, method string, args map[string]interface{}, data string) error { 83 | call := api.Call{ 84 | Method: method, 85 | Args: args, 86 | Data: data, 87 | } 88 | 89 | call.Call(ctx) 90 | defer call.Close() 91 | return pipe(ctx, call, call, stdin, stdout) 92 | } 93 | -------------------------------------------------------------------------------- /notification.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import "bytes" 4 | 5 | // NotificationPriority enumerates the priority level of the notification 6 | type NotificationPriority string 7 | 8 | const ( 9 | // Max notification priority 10 | Max NotificationPriority = "max" 11 | // High notification priority 12 | High NotificationPriority = "high" 13 | // Default notification priority 14 | Default NotificationPriority = "default" 15 | // Low notification priority 16 | Low NotificationPriority = "low" 17 | // Min notification priority 18 | Min NotificationPriority = "min" 19 | ) 20 | 21 | // NotificationButton represents a button shown in the notification bar 22 | type NotificationButton struct { 23 | Text string 24 | Action string 25 | } 26 | 27 | // NotificationOpt represents the options of a notification 28 | type NotificationOpt struct { 29 | Content string 30 | Sound bool 31 | Title string 32 | Viberate []int32 33 | Priority NotificationPriority 34 | LED struct { 35 | Color string 36 | On int 37 | Off int 38 | } 39 | Action string 40 | DeleteAction string 41 | Btn1 NotificationButton 42 | Btn2 NotificationButton 43 | Btn3 NotificationButton 44 | } 45 | 46 | // NotificationRemove removes the notification with the given id 47 | func NotificationRemove(id string) error { 48 | buf := bytes.NewBuffer([]byte{}) 49 | exec(nil, buf, "NotificationRemove", map[string]interface{}{ 50 | "id": id, 51 | }, "") 52 | return checkErr(buf.Bytes()) 53 | } 54 | 55 | // Notification creates a new notification with the given id and options 56 | func Notification(id string, opt NotificationOpt) error { 57 | in := bytes.NewBuffer([]byte{}) 58 | out := bytes.NewBuffer([]byte{}) 59 | in.WriteString(opt.Content) 60 | 61 | param := map[string]interface{}{ 62 | "priority": opt.Priority, 63 | "title": opt.Title, 64 | "sound": opt.Sound, 65 | "id": id, 66 | } 67 | if opt.LED.On > 0 && opt.LED.Off > 0 { 68 | param["led-on"] = opt.LED.On 69 | param["led-off"] = opt.LED.Off 70 | } 71 | if opt.Viberate != nil { 72 | param["viberate"] = opt.Viberate 73 | } 74 | for key, val := range map[string]string{ 75 | "led-color": opt.LED.Color, 76 | "action": opt.Action, 77 | "on_delete_action": opt.DeleteAction, 78 | "button_text_1": opt.Btn1.Text, 79 | "button_action_1": opt.Btn1.Action, 80 | "button_text_2": opt.Btn2.Text, 81 | "button_action_2": opt.Btn2.Action, 82 | "button_text_3": opt.Btn3.Text, 83 | "button_action_3": opt.Btn3.Action, 84 | } { 85 | if val == "" { 86 | continue 87 | } 88 | param[key] = val 89 | } 90 | 91 | if err := exec(in, out, "Notification", param, ""); err != nil { 92 | return err 93 | } 94 | return checkErr(out.Bytes()) 95 | } 96 | -------------------------------------------------------------------------------- /internal/intent/broadcast.go: -------------------------------------------------------------------------------- 1 | package intent 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os/exec" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Broadcast struct { 12 | User int 13 | Component string 14 | Data string 15 | Action string 16 | extraString map[string]string 17 | extraBool map[string]bool 18 | extraInt map[string]int 19 | extraLongs map[string][]int32 20 | extraStrings map[string][]string 21 | } 22 | 23 | func (c *Broadcast) AddBool(key string, val bool) { 24 | if c.extraBool == nil { 25 | c.extraBool = make(map[string]bool) 26 | } 27 | c.extraBool[key] = val 28 | } 29 | 30 | func (c *Broadcast) AddString(key string, val string) { 31 | if c.extraString == nil { 32 | c.extraString = make(map[string]string) 33 | } 34 | c.extraString[key] = val 35 | } 36 | 37 | func (c *Broadcast) AddInt(key string, val int) { 38 | if c.extraInt == nil { 39 | c.extraInt = make(map[string]int) 40 | } 41 | c.extraInt[key] = val 42 | } 43 | 44 | func (c *Broadcast) AddLongs(key string, val []int32) { 45 | if c.extraLongs == nil { 46 | c.extraLongs = make(map[string][]int32) 47 | } 48 | c.extraLongs[key] = val 49 | } 50 | 51 | func (c *Broadcast) AddStrings(key string, val []string) { 52 | if c.extraStrings == nil { 53 | c.extraStrings = make(map[string][]string) 54 | } 55 | c.extraStrings[key] = val 56 | } 57 | 58 | func (c *Broadcast) Send(ctx context.Context) { 59 | args := []string{"broadcast", "--user", strconv.Itoa(c.User), "-n", c.Component} 60 | if c.Data != "" { 61 | args = append(args, "-d", c.Data) 62 | } 63 | if c.Action != "" { 64 | args = append(args, "-a", c.Action) 65 | } 66 | if c.extraString != nil { 67 | for key, val := range c.extraString { 68 | args = append(args, "--es", key, val) 69 | } 70 | } 71 | if c.extraBool != nil { 72 | for key, val := range c.extraBool { 73 | args = append(args, "--ez", key, strconv.FormatBool(val)) 74 | } 75 | } 76 | if c.extraInt != nil { 77 | for key, val := range c.extraInt { 78 | args = append(args, "--ei", key, strconv.Itoa(val)) 79 | } 80 | } 81 | if c.extraLongs != nil { 82 | for key, val := range c.extraLongs { 83 | t := bytes.NewBuffer([]byte{}) 84 | for i, v := range val { 85 | t.WriteString(string(v)) 86 | if i+1 != len(val) { 87 | t.WriteRune(',') 88 | } 89 | } 90 | args = append(args, "--ela", key, t.String()) 91 | } 92 | } 93 | if c.extraStrings != nil { 94 | for key, val := range c.extraStrings { 95 | t := bytes.NewBuffer([]byte{}) 96 | for i, v := range val { 97 | t.WriteString(strings.Replace(v, ",", "\\,", -1)) 98 | if i+1 != len(val) { 99 | t.WriteRune(',') 100 | } 101 | } 102 | args = append(args, "--esa", key, t.String()) 103 | } 104 | } 105 | cmd := exec.CommandContext(ctx, "/data/data/com.termux/files/usr/bin/am", args...) 106 | go cmd.Run() 107 | } 108 | -------------------------------------------------------------------------------- /location.go: -------------------------------------------------------------------------------- 1 | package termux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | 8 | "github.com/eternal-flame-AD/go-termux/internal/chanbuf" 9 | ) 10 | 11 | // LocationProvider enumerates the location sources provided by the device 12 | type LocationProvider string 13 | 14 | const ( 15 | // GPS acquire location with GPS 16 | GPS LocationProvider = "gps" 17 | // Network acquire location using current network 18 | Network LocationProvider = "network" 19 | // Passive acquire location using passive methods 20 | Passive LocationProvider = "passive" 21 | ) 22 | 23 | // LocationRecord represents a location record provided by the device 24 | type LocationRecord struct { 25 | Latitude float64 `json:"latitude"` 26 | Longitude float64 `json:"longitude"` 27 | Altitude float64 `json:"altitude"` 28 | Accuracy float64 `json:"accuracy"` 29 | Bearing float64 `json:"bearing"` 30 | Speed float64 `json:"speed"` 31 | ElapsedMS int `json:"elapsedMs"` 32 | Provider string `json:"provider"` 33 | } 34 | 35 | func location(ctx context.Context, t string, provider LocationProvider) (*LocationRecord, error) { 36 | buf := bytes.NewBuffer([]byte{}) 37 | if err := execContext(ctx, nil, buf, "Location", map[string]interface{}{ 38 | "provider": string(provider), 39 | "request": t, 40 | }, ""); err != nil { 41 | return nil, err 42 | } 43 | res := buf.Bytes() 44 | if err := checkErr(res); err != nil { 45 | return nil, err 46 | } 47 | r := new(LocationRecord) 48 | if err := json.Unmarshal(res, r); err != nil { 49 | return nil, err 50 | } 51 | return r, nil 52 | } 53 | 54 | // LastLocation acquires the last known location of the device 55 | func LastLocation(ctx context.Context, provider LocationProvider) (*LocationRecord, error) { 56 | return location(ctx, "last", provider) 57 | } 58 | 59 | // Location acquires the current location of the device 60 | func Location(ctx context.Context, provider LocationProvider) (*LocationRecord, error) { 61 | return location(ctx, "once", provider) 62 | } 63 | 64 | // UpdatedLocation acquires the real-time location of the device from a channel 65 | func UpdatedLocation(ctx context.Context, provider LocationProvider) (<-chan struct { 66 | Location *LocationRecord 67 | Error error 68 | }, error) { 69 | response := make(chan []byte) 70 | ret := make(chan struct { 71 | Location *LocationRecord 72 | Error error 73 | }) 74 | if err := execContext(ctx, nil, chanbuf.BufToChan{ 75 | C: response, 76 | }, "Location", map[string]interface{}{ 77 | "provider": string(provider), 78 | "request": "updates", 79 | }, ""); err != nil { 80 | return nil, err 81 | } 82 | go func() { 83 | for { 84 | select { 85 | case <-ctx.Done(): 86 | close(ret) 87 | return 88 | case data := <-response: 89 | if err := checkErr(data); err != nil { 90 | ret <- struct { 91 | Location *LocationRecord 92 | Error error 93 | }{nil, err} 94 | continue 95 | } 96 | l := new(LocationRecord) 97 | if err := json.Unmarshal(data, l); err != nil { 98 | ret <- struct { 99 | Location *LocationRecord 100 | Error error 101 | }{nil, err} 102 | continue 103 | } 104 | ret <- struct { 105 | Location *LocationRecord 106 | Error error 107 | }{l, nil} 108 | } 109 | } 110 | }() 111 | return ret, nil 112 | } 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [eternal-flame-AD] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------