1package libkb
2
3import (
4	"fmt"
5	"sync"
6	"time"
7
8	"github.com/keybase/client/go/protocol/keybase1"
9	"github.com/keybase/go-framed-msgpack-rpc/rpc"
10)
11
12// MobileAppState tracks the state of foreground/background status of the app
13// in which the service is running in.
14type MobileAppState struct {
15	Contextified
16	sync.Mutex
17	state     keybase1.MobileAppState
18	updateChs []chan keybase1.MobileAppState
19
20	// mtime is the time at which the appstate first switched to the current state.
21	// It is a monotonic timestamp and should only be used relatively.
22	mtime *time.Time
23}
24
25func NewMobileAppState(g *GlobalContext) *MobileAppState {
26	return &MobileAppState{
27		Contextified: NewContextified(g),
28		state:        keybase1.MobileAppState_FOREGROUND,
29		mtime:        nil,
30	}
31}
32
33// NextUpdate returns a channel that triggers when the app state changes
34func (a *MobileAppState) NextUpdate(lastState *keybase1.MobileAppState) chan keybase1.MobileAppState {
35	a.Lock()
36	defer a.Unlock()
37	ch := make(chan keybase1.MobileAppState, 1)
38	if lastState != nil && *lastState != a.state {
39		ch <- a.state
40	} else {
41		a.updateChs = append(a.updateChs, ch)
42	}
43	return ch
44}
45
46func (a *MobileAppState) updateLocked(state keybase1.MobileAppState) {
47	if a.state != state {
48		a.G().Log.Debug("MobileAppState.Update: useful update: %v, we are currently in state: %v",
49			state, a.state)
50		a.G().PerfLog.Debug("MobileAppState.Update: useful update: %v, we are currently in state: %v",
51			state, a.state)
52		a.state = state
53		t := time.Now()
54		a.mtime = &t // only update mtime if we're changing state
55		for _, ch := range a.updateChs {
56			ch <- state
57		}
58		a.updateChs = nil
59
60		// cancel RPCs if we go into the background
61		switch a.state {
62		case keybase1.MobileAppState_BACKGROUND:
63			a.G().RPCCanceler.CancelLiveContexts(RPCCancelerReasonBackground)
64		default:
65			// Nothing to do for other states.
66		}
67	} else {
68		a.G().Log.Debug("MobileAppState.Update: ignoring update: %v, we are currently in state: %v",
69			state, a.state)
70	}
71}
72
73func (a *MobileAppState) UpdateWithCheck(state keybase1.MobileAppState,
74	check func(keybase1.MobileAppState) bool) {
75	defer a.G().Trace(fmt.Sprintf("MobileAppState.UpdateWithCheck(%v)", state), nil)()
76	a.Lock()
77	defer a.Unlock()
78	if check(a.state) {
79		a.updateLocked(state)
80	} else {
81		a.G().Log.Debug("MobileAppState.UpdateWithCheck: skipping update, failed check")
82	}
83}
84
85// Update updates the current app state, and notifies any waiting calls from NextUpdate
86func (a *MobileAppState) Update(state keybase1.MobileAppState) {
87	defer a.G().Trace(fmt.Sprintf("MobileAppState.Update(%v)", state), nil)()
88	a.Lock()
89	defer a.Unlock()
90	a.updateLocked(state)
91}
92
93// State returns the current app state
94func (a *MobileAppState) State() keybase1.MobileAppState {
95	a.Lock()
96	defer a.Unlock()
97	return a.state
98}
99
100func (a *MobileAppState) StateAndMtime() (keybase1.MobileAppState, *time.Time) {
101	a.Lock()
102	defer a.Unlock()
103	return a.state, a.mtime
104}
105
106// --------------------------------------------------
107
108// MobileNetState tracks the state of the network status of the app in which
109// the service is running in.
110type MobileNetState struct {
111	Contextified
112	sync.Mutex
113	state     keybase1.MobileNetworkState
114	updateChs []chan keybase1.MobileNetworkState
115}
116
117func NewMobileNetState(g *GlobalContext) *MobileNetState {
118	return &MobileNetState{
119		Contextified: NewContextified(g),
120		state:        keybase1.MobileNetworkState_NOTAVAILABLE,
121	}
122}
123
124// NextUpdate returns a channel that triggers when the network state changes
125func (a *MobileNetState) NextUpdate(lastState *keybase1.MobileNetworkState) chan keybase1.MobileNetworkState {
126	a.Lock()
127	defer a.Unlock()
128	ch := make(chan keybase1.MobileNetworkState, 1)
129	if lastState != nil && *lastState != a.state {
130		ch <- a.state
131	} else {
132		a.updateChs = append(a.updateChs, ch)
133	}
134	return ch
135}
136
137// Update updates the current network state, and notifies any waiting calls
138// from NextUpdate
139func (a *MobileNetState) Update(state keybase1.MobileNetworkState) {
140	defer a.G().Trace(fmt.Sprintf("MobileNetState.Update(%v)", state), nil)()
141	a.Lock()
142	defer a.Unlock()
143	if a.state != state {
144		a.G().Log.Debug("MobileNetState.Update: useful update: %v, we are currently in state: %v",
145			state, a.state)
146		a.state = state
147		for _, ch := range a.updateChs {
148			ch <- state
149		}
150		a.updateChs = nil
151	} else {
152		a.G().Log.Debug("MobileNetState.Update: ignoring update: %v, we are currently in state: %v",
153			state, a.state)
154	}
155}
156
157// State returns the current network state
158func (a *MobileNetState) State() keybase1.MobileNetworkState {
159	a.Lock()
160	defer a.Unlock()
161	return a.state
162}
163
164// --------------------------------------------------
165
166type DesktopAppState struct {
167	Contextified
168	sync.Mutex
169	provider         rpc.Transporter
170	suspended        bool
171	locked           bool
172	updateSuspendChs []chan bool
173}
174
175func NewDesktopAppState(g *GlobalContext) *DesktopAppState {
176	d := &DesktopAppState{Contextified: NewContextified(g)}
177	g.PushShutdownHook(func(mctx MetaContext) error {
178		d.Lock()
179		defer d.Unlock()
180		// reset power state on shutdown
181		d.resetLocked()
182		return nil
183	})
184	return d
185}
186
187func (a *DesktopAppState) NextSuspendUpdate(lastState *bool) chan bool {
188	a.Lock()
189	defer a.Unlock()
190	ch := make(chan bool, 1)
191	if lastState != nil && *lastState != a.suspended {
192		ch <- a.suspended
193	} else {
194		a.updateSuspendChs = append(a.updateSuspendChs, ch)
195	}
196	return ch
197}
198
199// event from power monitor
200// https://electronjs.org/docs/api/power-monitor
201func (a *DesktopAppState) Update(mctx MetaContext, event string, provider rpc.Transporter) {
202	mctx.Debug("DesktopAppState.Update(%v)", event)
203	a.Lock()
204	defer a.Unlock()
205	a.provider = provider
206	switch event {
207	case "suspend":
208		a.suspended = true
209	case "resume":
210		a.suspended = false
211	case "shutdown":
212	case "lock-screen":
213		a.locked = true
214	case "unlock-screen":
215		a.suspended = false
216		a.locked = false
217	}
218	for _, ch := range a.updateSuspendChs {
219		ch <- a.suspended
220	}
221	a.updateSuspendChs = nil
222}
223
224func (a *DesktopAppState) Disconnected(provider rpc.Transporter) {
225	a.Lock()
226	defer a.Unlock()
227	theProvider := provider == a.provider
228	a.G().Log.Debug("DesktopAppState.Disconnected(%v)", theProvider)
229	if theProvider {
230		a.provider = nil
231		// The connection to electron has been severed. We won't get any more power
232		// status updates from it. So act as though the machine is on in the default state.
233		a.resetLocked()
234	}
235}
236
237func (a *DesktopAppState) AwakeAndUnlocked(mctx MetaContext) bool {
238	a.Lock()
239	defer a.Unlock()
240	return !a.suspended && !a.locked
241}
242
243func (a *DesktopAppState) resetLocked() {
244	a.suspended = false
245	a.locked = false
246}
247