Revision control

Copy as Markdown

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
public extension Notification.Name {
static let constellationStateUpdate = Notification.Name("constellationStateUpdate")
}
public struct ConstellationState {
public let localDevice: Device?
public let remoteDevices: [Device]
}
public class DeviceConstellation {
var constellationState: ConstellationState?
let account: PersistedFirefoxAccount
init(account: PersistedFirefoxAccount) {
self.account = account
}
/// Get local + remote devices synchronously.
/// Note that this state might be empty, which should handle by calling `refreshState()`
/// A `.constellationStateUpdate` notification is fired if the device list changes at any time.
public func state() -> ConstellationState? {
return constellationState
}
/// Refresh the list of remote devices.
/// A `.constellationStateUpdate` notification might get fired once the new device list is fetched.
public func refreshState() {
DispatchQueue.global().async {
FxALog.info("Refreshing device list...")
do {
let devices = try self.account.getDevices(ignoreCache: true)
let localDevice = devices.first { $0.isCurrentDevice }
if localDevice?.pushEndpointExpired ?? false {
FxALog.debug("Current device needs push endpoint registration.")
}
let remoteDevices = devices.filter { !$0.isCurrentDevice }
let newState = ConstellationState(localDevice: localDevice, remoteDevices: remoteDevices)
self.constellationState = newState
FxALog.debug("Refreshed device list; saw \(devices.count) device(s).")
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .constellationStateUpdate,
object: nil,
userInfo: ["newState": newState]
)
}
} catch {
FxALog.error("Failure fetching the device list: \(error).")
return
}
}
}
/// Updates the local device name.
public func setLocalDeviceName(name: String) {
DispatchQueue.global().async {
do {
try self.account.setDeviceName(name)
// Update our list of devices in the background to reflect the change.
self.refreshState()
} catch {
FxALog.error("Failure changing the local device name: \(error).")
}
}
}
/// Poll for device events we might have missed (e.g. Push notification missed, or device offline).
/// Your app should probably call this on a regular basic (e.g. once a day).
public func pollForCommands(completionHandler: @escaping (Result<[IncomingDeviceCommand], Error>) -> Void) {
DispatchQueue.global().async {
do {
let events = try self.account.pollDeviceCommands()
DispatchQueue.main.async { completionHandler(.success(events)) }
} catch {
DispatchQueue.main.async { completionHandler(.failure(error)) }
}
}
}
/// Send an event to another device such as Send Tab.
public func sendEventToDevice(targetDeviceId: String, e: DeviceEventOutgoing) {
DispatchQueue.global().async {
do {
switch e {
case let .sendTab(title, url): do {
try self.account.sendSingleTab(targetDeviceId: targetDeviceId, title: title, url: url)
}
case let .closeTabs(urls):
_ = try self.account.closeTabs(targetDeviceId: targetDeviceId, urls: urls)
}
} catch {
FxALog.error("Error sending event to another device: \(error).")
}
}
}
/// Register the local AutoPush subscription with the FxA server.
public func setDevicePushSubscription(sub: DevicePushSubscription) {
DispatchQueue.global().async {
do {
try self.account.setDevicePushSubscription(sub: sub)
} catch {
FxALog.error("Failure setting push subscription: \(error).")
}
}
}
/// Once Push has decrypted a payload, send the payload to this method
/// which will tell the app what to do with it in form of an `AccountEvent`.
public func handlePushMessage(pushPayload: String,
completionHandler: @escaping (Result<AccountEvent, Error>) -> Void)
{
DispatchQueue.global().async {
do {
let event = try self.account.handlePushMessage(payload: pushPayload)
self.processAccountEvent(event)
DispatchQueue.main.async { completionHandler(.success(event)) }
} catch {
DispatchQueue.main.async { completionHandler(.failure(error)) }
}
}
}
/// This allows us to be helpful in certain circumstances e.g. refreshing the device list
/// if we see a "device disconnected" push notification.
func processAccountEvent(_ event: AccountEvent) {
switch event {
case .deviceDisconnected, .deviceConnected: refreshState()
default: return
}
}
func initDevice(name: String, type: DeviceType, capabilities: [DeviceCapability]) {
// This method is called by `FxAccountManager` on its own asynchronous queue, hence
// no wrapping in a `DispatchQueue.global().async`.
assert(!Thread.isMainThread)
do {
try account.initializeDevice(name: name, deviceType: type, supportedCapabilities: capabilities)
} catch {
FxALog.error("Failure initializing device: \(error).")
}
}
func ensureCapabilities(capabilities: [DeviceCapability]) {
// This method is called by `FxAccountManager` on its own asynchronous queue, hence
// no wrapping in a `DispatchQueue.global().async`.
assert(!Thread.isMainThread)
do {
try account.ensureCapabilities(supportedCapabilities: capabilities)
} catch {
FxALog.error("Failure ensuring device capabilities: \(error).")
}
}
}
public enum DeviceEventOutgoing {
case sendTab(title: String, url: String)
case closeTabs(urls: [String])
}