diff --git a/Makefile b/Makefile index bb55b0c..0122087 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ example-start: - go run example/tracker.go \ No newline at end of file + go run example/example.go \ No newline at end of file diff --git a/example/tracker.go b/example/example.go similarity index 54% rename from example/tracker.go rename to example/example.go index 014ad5d..9a0d000 100644 --- a/example/tracker.go +++ b/example/example.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/prashantgupta24/activity-tracker/src/activity" + "github.com/prashantgupta24/activity-tracker/pkg/tracker" ) func main() { @@ -12,7 +12,7 @@ func main() { timeToCheck := 5 - activityTracker := &activity.ActivityTracker{ + activityTracker := &tracker.Instance{ TimeToCheck: time.Duration(timeToCheck), } heartbeatCh, quitActivityTracker := activityTracker.Start() @@ -23,9 +23,14 @@ func main() { select { case heartbeat := <-heartbeatCh: if !heartbeat.IsActivity { - fmt.Printf("no activity detected in the last %v seconds\n", int(timeToCheck)) + fmt.Printf("no activity detected in the last %v seconds\n\n", int(timeToCheck)) } else { - fmt.Printf("activity detected in the last %v seconds\n", int(timeToCheck)) + fmt.Printf("activity detected in the last %v seconds. ", int(timeToCheck)) + fmt.Printf("Activity type:\n") + for activity, time := range heartbeat.Activity { + fmt.Printf("%v at %v\n", activity.ActivityType, time) + } + fmt.Println() } case <-timeToKill.C: fmt.Println("time to kill app") diff --git a/src/mouse/mouse.go b/internal/pkg/mouse/mouse.go similarity index 81% rename from src/mouse/mouse.go rename to internal/pkg/mouse/mouse.go index dae91b6..b1cb24c 100644 --- a/src/mouse/mouse.go +++ b/internal/pkg/mouse/mouse.go @@ -1,6 +1,8 @@ package mouse -import "github.com/go-vgo/robotgo" +import ( + "github.com/go-vgo/robotgo" +) type Position struct { MouseX int diff --git a/internal/pkg/service/mouseClickHandler.go b/internal/pkg/service/mouseClickHandler.go new file mode 100644 index 0000000..1994828 --- /dev/null +++ b/internal/pkg/service/mouseClickHandler.go @@ -0,0 +1,64 @@ +package service + +import ( + "log" + "time" + + "github.com/go-vgo/robotgo" + "github.com/prashantgupta24/activity-tracker/pkg/activity" +) + +type MouseClickHandler struct { + tickerCh chan struct{} +} + +func (m *MouseClickHandler) Start(activityCh chan *activity.Type) { + m.tickerCh = make(chan struct{}) + registrationFree := make(chan struct{}) + + go func() { + go addMouseClickRegistration(activityCh, registrationFree) //run once before first check + for range m.tickerCh { + log.Printf("mouse clicker checked at : %v\n", time.Now()) + select { + case _, ok := <-registrationFree: + if ok { + //log.Printf("registration free for mouse click \n") + go addMouseClickRegistration(activityCh, registrationFree) + } else { + //log.Printf("error : channel closed \n") + return + } + default: + //log.Printf("registration is busy for mouse click handler, do nothing\n") + } + } + log.Printf("stopping click handler") + return + }() +} + +func (m *MouseClickHandler) Trigger() { + //doing it the non-blocking sender way + select { + case m.tickerCh <- struct{}{}: + default: + //service is blocked, handle it somehow? + } +} +func (m *MouseClickHandler) Close() { + close(m.tickerCh) +} + +func addMouseClickRegistration(activityCh chan *activity.Type, registrationFree chan struct{}) { + log.Printf("adding reg \n") + mleft := robotgo.AddEvent("mleft") + if mleft { + //log.Printf("mleft clicked \n") + activityCh <- &activity.Type{ + ActivityType: activity.MOUSE_LEFT_CLICK, + } + registrationFree <- struct{}{} + return + } +} diff --git a/internal/pkg/service/mouseCursorHandler.go b/internal/pkg/service/mouseCursorHandler.go new file mode 100644 index 0000000..abba577 --- /dev/null +++ b/internal/pkg/service/mouseCursorHandler.go @@ -0,0 +1,76 @@ +package service + +import ( + "log" + "time" + + "github.com/prashantgupta24/activity-tracker/internal/pkg/mouse" + "github.com/prashantgupta24/activity-tracker/pkg/activity" +) + +type MouseCursorHandler struct { + tickerCh chan struct{} +} + +type cursorInfo struct { + didCursorMove bool + currentMousePos *mouse.Position +} + +func (m *MouseCursorHandler) Start(activityCh chan *activity.Type) { + + m.tickerCh = make(chan struct{}) + + go func() { + lastMousePos := mouse.GetPosition() + for range m.tickerCh { + log.Printf("mouse cursor checked at : %v\n", time.Now()) + commCh := make(chan *cursorInfo) + go checkCursorChange(commCh, lastMousePos) + select { + case cursorInfo := <-commCh: + if cursorInfo.didCursorMove { + activityCh <- &activity.Type{ + ActivityType: activity.MOUSE_CURSOR_MOVEMENT, + } + lastMousePos = cursorInfo.currentMousePos + } + case <-time.After(timeout * time.Millisecond): + //timeout, do nothing + log.Printf("timeout happened after %vms while checking mouse cursor handler", timeout) + } + } + log.Printf("stopping cursor handler") + return + }() +} + +func (m *MouseCursorHandler) Trigger() { + //doing it the non-blocking sender way + select { + case m.tickerCh <- struct{}{}: + default: + //service is blocked, handle it somehow? + } +} +func (m *MouseCursorHandler) Close() { + close(m.tickerCh) +} + +func checkCursorChange(commCh chan *cursorInfo, lastMousePos *mouse.Position) { + currentMousePos := mouse.GetPosition() + //log.Printf("current mouse position: %v\n", currentMousePos) + //log.Printf("last mouse position: %v\n", lastMousePos) + if currentMousePos.MouseX == lastMousePos.MouseX && + currentMousePos.MouseY == lastMousePos.MouseY { + commCh <- &cursorInfo{ + didCursorMove: false, + currentMousePos: nil, + } + } else { + commCh <- &cursorInfo{ + didCursorMove: true, + currentMousePos: currentMousePos, + } + } +} diff --git a/internal/pkg/service/screenChangeHandler.go b/internal/pkg/service/screenChangeHandler.go new file mode 100644 index 0000000..788e735 --- /dev/null +++ b/internal/pkg/service/screenChangeHandler.go @@ -0,0 +1,79 @@ +package service + +import ( + "log" + "time" + + "github.com/go-vgo/robotgo" + "github.com/prashantgupta24/activity-tracker/pkg/activity" +) + +type ScreenChangeHandler struct { + tickerCh chan struct{} +} + +type screenInfo struct { + didScreenChange bool + currentPixelColor string +} + +func (s *ScreenChangeHandler) Start(activityCh chan *activity.Type) { + + s.tickerCh = make(chan struct{}) + + go func() { + screenSizeX, screenSizeY := robotgo.GetScreenSize() + pixelPointX := int(screenSizeX / 2) + pixelPointY := int(screenSizeY / 2) + lastPixelColor := robotgo.GetPixelColor(pixelPointX, pixelPointY) + for range s.tickerCh { + log.Printf("screen change checked at : %v\n", time.Now()) + commCh := make(chan *screenInfo) + go checkScreenChange(commCh, lastPixelColor, pixelPointX, pixelPointY) + select { + case screenInfo := <-commCh: + if screenInfo.didScreenChange { + activityCh <- &activity.Type{ + ActivityType: activity.SCREEN_CHANGE, + } + lastPixelColor = screenInfo.currentPixelColor + } + case <-time.After(timeout * time.Millisecond): + //timeout, do nothing + log.Printf("timeout happened after %vms while checking screen change handler", timeout) + } + } + log.Printf("stopping screen change handler") + return + }() +} + +func (s *ScreenChangeHandler) Trigger() { + //doing it the non-blocking sender way + select { + case s.tickerCh <- struct{}{}: + default: + //service is blocked, handle it somehow? + } +} +func (s *ScreenChangeHandler) Close() { + close(s.tickerCh) +} + +func checkScreenChange(commCh chan *screenInfo, lastPixelColor string, pixelPointX, pixelPointY int) { + currentPixelColor := robotgo.GetPixelColor(pixelPointX, pixelPointY) + // log.Printf("current pixel color: %v\n", currentPixelColor) + // log.Printf("last pixel color: %v\n", lastPixelColor) + //robotgo.MoveMouse(pixelPointX, pixelPointY) + if lastPixelColor != currentPixelColor { + commCh <- &screenInfo{ + didScreenChange: true, + currentPixelColor: currentPixelColor, + } + } else { //comment this section out to test timeout logic + commCh <- &screenInfo{ + didScreenChange: false, + currentPixelColor: "", + } + } +} diff --git a/internal/pkg/service/service.go b/internal/pkg/service/service.go new file mode 100644 index 0000000..d8890c5 --- /dev/null +++ b/internal/pkg/service/service.go @@ -0,0 +1,13 @@ +package service + +import "github.com/prashantgupta24/activity-tracker/pkg/activity" + +const ( + timeout = 100 //ms +) + +type Instance interface { + Start(chan *activity.Type) + Trigger() + Close() +} diff --git a/pkg/activity/types.go b/pkg/activity/types.go new file mode 100644 index 0000000..aeded92 --- /dev/null +++ b/pkg/activity/types.go @@ -0,0 +1,13 @@ +package activity + +type activityType string + +const ( + MOUSE_CURSOR_MOVEMENT activityType = "cursor-move" + MOUSE_LEFT_CLICK activityType = "left-mouse-click" + SCREEN_CHANGE activityType = "screen-change" +) + +type Type struct { + ActivityType activityType +} diff --git a/pkg/tracker/tracker.go b/pkg/tracker/tracker.go new file mode 100644 index 0000000..dc8c8cd --- /dev/null +++ b/pkg/tracker/tracker.go @@ -0,0 +1,96 @@ +package tracker + +import ( + "log" + "time" + + "github.com/prashantgupta24/activity-tracker/internal/pkg/service" + "github.com/prashantgupta24/activity-tracker/pkg/activity" +) + +const ( + preHeartbeatTime = time.Millisecond * 100 +) + +func (tracker *Instance) Start() (heartbeatCh chan *Heartbeat, quit chan struct{}) { + + //register service handlers + tracker.registerHandlers(&service.MouseClickHandler{}, &service.MouseCursorHandler{}, + &service.ScreenChangeHandler{}) + + //returned channels + heartbeatCh = make(chan *Heartbeat, 1) + quit = make(chan struct{}) + + go func(tracker *Instance) { + timeToCheck := tracker.TimeToCheck + //tickers + tickerHeartbeat := time.NewTicker(time.Second * timeToCheck) + tickerWorker := time.NewTicker(time.Second*timeToCheck - preHeartbeatTime) + + activities := makeActivityMap() + + for { + select { + case <-tickerWorker.C: + log.Printf("tracker worker working at %v\n", time.Now()) + //time to trigger all registered services + for service := range tracker.services { + service.Trigger() + } + case <-tickerHeartbeat.C: + log.Printf("tracker heartbeat checking at %v\n", time.Now()) + var heartbeat *Heartbeat + if len(activities) == 0 { + //log.Printf("no activity detected in the last %v seconds ...\n", int(timeToCheck)) + heartbeat = &Heartbeat{ + IsActivity: false, + Activity: nil, + Time: time.Now(), + } + } else { + //log.Printf("activity detected in the last %v seconds ...\n", int(timeToCheck)) + heartbeat = &Heartbeat{ + IsActivity: true, + Activity: activities, + Time: time.Now(), + } + + } + heartbeatCh <- heartbeat + activities = makeActivityMap() //reset the activities map + case activity := <-tracker.activityCh: + activities[activity] = time.Now() + //log.Printf("activity received: %#v\n", activity) + case <-quit: + log.Printf("stopping activity tracker\n") + //close all services for a clean exit + for service := range tracker.services { + service.Close() + } + return + } + } + }(tracker) + + return heartbeatCh, quit +} + +func makeActivityMap() map[*activity.Type]time.Time { + activityMap := make(map[*activity.Type]time.Time) + return activityMap +} + +func (tracker *Instance) registerHandlers(services ...service.Instance) { + + tracker.services = make(map[service.Instance]bool) + tracker.activityCh = make(chan *activity.Type, len(services)) // number based on types of activities being tracked + + for _, service := range services { + service.Start(tracker.activityCh) + if _, ok := tracker.services[service]; !ok { //duplicate registration prevention + tracker.services[service] = true + } + + } +} diff --git a/pkg/tracker/types.go b/pkg/tracker/types.go new file mode 100644 index 0000000..dd46178 --- /dev/null +++ b/pkg/tracker/types.go @@ -0,0 +1,20 @@ +package tracker + +import ( + "time" + + "github.com/prashantgupta24/activity-tracker/internal/pkg/service" + "github.com/prashantgupta24/activity-tracker/pkg/activity" +) + +type Instance struct { + TimeToCheck time.Duration + activityCh chan *activity.Type + services map[service.Instance]bool +} + +type Heartbeat struct { + IsActivity bool + Activity map[*activity.Type]time.Time + Time time.Time +} diff --git a/src/activity/activityTracker.go b/src/activity/activityTracker.go deleted file mode 100644 index d7bbee5..0000000 --- a/src/activity/activityTracker.go +++ /dev/null @@ -1,116 +0,0 @@ -package activity - -import ( - "log" - "time" - - "github.com/go-vgo/robotgo" - "github.com/prashantgupta24/activity-tracker/src/mouse" -) - -func (tracker *ActivityTracker) Start() (heartbeatCh chan *Heartbeat, quit chan struct{}) { - - comm, quitMouseClickHandler := isMouseClicked(tracker) - - heartbeatCh = make(chan *Heartbeat, 1) - quit = make(chan struct{}) - - go func(tracker *ActivityTracker, heartbeatCh chan *Heartbeat, quit chan struct{}) { - timeToCheck := tracker.TimeToCheck - ticker := time.NewTicker(time.Second * timeToCheck) - isIdle := true - lastMousePos := mouse.GetPosition() - for { - select { - case <-ticker.C: - //log.Printf("tracker checking at %v\n", time.Now()) - currentMousePos := mouse.GetPosition() - var heartbeat *Heartbeat - if isIdle && isPointerIdle(currentMousePos, lastMousePos) { - //log.Printf("no activity detected in the last %v seconds ...\n", int(timeToCheck)) - heartbeat = &Heartbeat{ - IsActivity: false, - Time: time.Now(), - } - } else { - //log.Printf("activity detected in the last %v seconds ...\n", int(timeToCheck)) - heartbeat = &Heartbeat{ - IsActivity: true, - Time: time.Now(), - } - lastMousePos = currentMousePos - } - heartbeatCh <- heartbeat - isIdle = true - case <-comm: - isIdle = false - //log.Printf("value received: %v\n", isIdle) - case <-quit: - log.Printf("stopping activity tracker\n") - quitMouseClickHandler <- struct{}{} - //robotgo.StopEvent() - return - } - } - }(tracker, heartbeatCh, quit) - - return heartbeatCh, quit -} - -func isPointerIdle(currentMousePos, lastMousePos *mouse.Position) bool { - //log.Printf("current mouse position: %v\n", currentMousePos) - //log.Printf("last mouse position: %v\n", lastMousePos) - if currentMousePos.MouseX == lastMousePos.MouseX && - currentMousePos.MouseY == lastMousePos.MouseY { - return true - } - return false -} - -func isMouseClicked(tracker *ActivityTracker) (clickComm, quit chan struct{}) { - ticker := time.NewTicker(time.Second * tracker.TimeToCheck) - clickComm = make(chan struct{}, 1) - quit = make(chan struct{}) - registrationFree := make(chan struct{}) - go func() { - isRunning := false - for { - select { - case <-ticker.C: - //log.Printf("mouse clicker ticked at : %v\n", time.Now()) - if !isRunning { - isRunning = true - go func(registrationFree chan struct{}) { - //log.Printf("adding reg \n") - mleft := robotgo.AddEvent("mleft") - if mleft { - //log.Printf("mleft clicked \n") - clickComm <- struct{}{} - registrationFree <- struct{}{} - return - } - }(registrationFree) - } - - select { - case _, ok := <-registrationFree: - if ok { - //log.Printf("registration free for mouse click \n") - isRunning = false - } else { - //log.Printf("Channel closed \n") - } - default: - //log.Printf("registration is busy for mouse click handler\n") - isRunning = true - } - - case <-quit: - log.Printf("stopping click handler") - close(clickComm) - return - } - } - }() - return clickComm, quit -} diff --git a/src/activity/types.go b/src/activity/types.go deleted file mode 100644 index daad7d5..0000000 --- a/src/activity/types.go +++ /dev/null @@ -1,12 +0,0 @@ -package activity - -import "time" - -type ActivityTracker struct { - TimeToCheck time.Duration -} - -type Heartbeat struct { - IsActivity bool - Time time.Time -}