Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@

- iOS networking should use the reqwest backend, instead of failing ([#1032](https://github.com/mozilla/application-services/pull/1032))

## FxA

### What's new

- Android only: Added device registration and Firefox Send Tab capability support. Your app can opt into this by calling the `FirefoxAccount.initializeDevice` method. ([#676](https://github.com/mozilla/application-services/pull/676))


# v0.26.0 (_2018-04-17_)

[Full Changelog](https://github.com/mozilla/application-services/compare/v0.25.2...v0.26.0)
Expand Down
105 changes: 98 additions & 7 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions components/fxa-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ license = "MPL-2.0"
base64 = "0.10.1"
byteorder = "1.3.1"
bytes = "0.4"
ece = "0.1"
failure = "0.1.3"
hawk = { version = "1.0.5", optional = true }
hex = "0.3.2"
Expand All @@ -21,6 +22,7 @@ ring = "0.14.5"
serde = { version = "1.0.79", features = ["rc"] }
serde_derive = "1.0.79"
serde_json = "1.0.28"
sync15 = { path = "../sync15" }
untrusted = "0.6.2"
url = "1.7.1"
ffi-support = { path = "../support/ffi" }
Expand All @@ -30,6 +32,8 @@ rc_crypto = { path = "../support/rc_crypto" }
[dev-dependencies]
cli-support = { path = "../support/cli" }
force-viaduct-reqwest = { path = "../support/force-viaduct-reqwest" }
dialoguer = "0.3.0"
webbrowser = "0.4.0"

[build-dependencies]
prost-build = "0.5"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* 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/. */

package mozilla.appservices.fxaclient

data class TabHistoryEntry(
val title: String,
val url: String
)

// https://proandroiddev.com/til-when-is-when-exhaustive-31d69f630a8b
val <T> T.exhaustive: T
get() = this

sealed class AccountEvent {
// A tab with all its history entries (back button).
class TabReceived(val from: Device?, val entries: Array<TabHistoryEntry>) : AccountEvent()

companion object {
private fun fromMessage(msg: MsgTypes.AccountEvent): AccountEvent {
when (msg.type) {
MsgTypes.AccountEvent.AccountEventType.TAB_RECEIVED -> {
val data = msg.tabReceivedData
return TabReceived(
from = if (data.hasFrom()) Device.fromMessage(data.from) else null,
entries = data.entriesList.map {
TabHistoryEntry(title = it.title, url = it.url)
}.toTypedArray()
)
}
}.exhaustive
}
internal fun fromCollectionMessage(msg: MsgTypes.AccountEvents): Array<AccountEvent> {
return msg.eventsList.map {
fromMessage(it)
}.toTypedArray()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* 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/. */

package mozilla.appservices.fxaclient

data class Device(
val id: String,
val displayName: String,
val deviceType: Type,
val pushSubscription: PushSubscription?,
val pushEndpointExpired: Boolean,
val isCurrentDevice: Boolean,
val lastAccessTime: Long?,
val capabilities: List<Capability>
) {
enum class Capability {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bit of a chuckle at this, because we used to call these "capabilities" on the server side until we needed more data and turned them into "commands".

If we end up removing support for non-megazord builds, does that clear a future path to separating the send-tab stuff out into its own distinct component, at which point we might not need "capability" flags like this inside the core FxA component? (For the future obviously, this seems fine for now).

Copy link
Contributor Author

@eoger eoger Apr 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bit of a chuckle at this, because we used to call these "capabilities" on the server side until we needed more data and turned them into "commands".

Yup my rationale is that the clients don't need the Commands "values" as the UI only care about "capabilities", but I might be wrong in the future :)

If we end up removing support for non-megazord builds, does that clear a future path to separating the send-tab stuff out into its own distinct component, at which point we might not need "capability" flags like this inside the core FxA component?

Yes it might make it easier, we should look into that later this year.

SEND_TAB;

companion object {
internal fun fromMessage(msg: MsgTypes.Device.Capability): Capability {
return when (msg) {
MsgTypes.Device.Capability.SEND_TAB -> SEND_TAB
}.exhaustive
}
}
}

enum class Type {
DESKTOP,
MOBILE,
UNKNOWN;

companion object {
internal fun fromMessage(msg: MsgTypes.Device.Type): Type {
return when (msg) {
MsgTypes.Device.Type.DESKTOP -> DESKTOP
MsgTypes.Device.Type.MOBILE -> MOBILE
else -> UNKNOWN
}
}
}

fun toNumber(): Int {
return MsgTypes.Device.Type.DESKTOP.number
}
}
data class PushSubscription(
val endpoint: String,
val publicKey: String,
val authKey: String
) {
companion object {
internal fun fromMessage(msg: MsgTypes.Device.PushSubscription): PushSubscription {
return PushSubscription(
endpoint = msg.endpoint,
publicKey = msg.publicKey,
authKey = msg.authKey
)
}
}
}
companion object {
internal fun fromMessage(msg: MsgTypes.Device): Device {
return Device(
id = msg.id,
displayName = msg.displayName,
deviceType = Type.fromMessage(msg.type),
pushSubscription = if (msg.hasPushSubscription()) {
PushSubscription.fromMessage(msg.pushSubscription)
} else null,
pushEndpointExpired = msg.pushEndpointExpired,
isCurrentDevice = msg.isCurrentDevice,
lastAccessTime = if (msg.hasLastAccessTime()) msg.lastAccessTime else null,
capabilities = msg.capabilitiesList.map { Capability.fromMessage(it) }
)
}
internal fun fromCollectionMessage(msg: MsgTypes.Devices): Array<Device> {
return msg.devicesList.map {
fromMessage(it)
}.toTypedArray()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package mozilla.appservices.fxaclient

import android.util.Log
import com.sun.jna.Native
import com.sun.jna.Pointer
import mozilla.appservices.fxaclient.rust.FxaHandle
import mozilla.appservices.fxaclient.rust.LibFxAFFI
import mozilla.appservices.fxaclient.rust.RustError
import mozilla.appservices.support.toNioDirectBuffer
import java.util.concurrent.atomic.AtomicLong

/**
Expand Down Expand Up @@ -252,6 +254,150 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
}.getAndConsumeRustString()
}

/**
* Update the push subscription details for the current device.
* This needs to be called every time a push subscription is modified or expires.
*
* This performs network requests, and should not be used on the main thread.
*
* @param endpoint Push callback URL
* @param endpoint Public key used to encrypt push payloads
* @param endpoint Auth key used to encrypt push payloads
*/
fun setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_set_push_subscription(this.handle.get(), endpoint, publicKey, authKey, e)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should the caller do if the network request fails here? Do they need to remember that it failed and retry again at some later time? Let's discuss with @grigoryk at the next sync-up to ensure we have consistent expectations around how to handle failed device data updates.


/**
* Update the display name (as shown in the FxA device manager, or the Send Tab target list)
* for the current device.
*
* This performs network requests, and should not be used on the main thread.
*
* @param displayName The current device display name
*/
fun setDeviceDisplayName(displayName: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_set_device_name(this.handle.get(), displayName, e)
}
}

/**
* Retrieves the list of the connected devices in the current account, including the current one.
*
* This performs network requests, and should not be used on the main thread.
*/
fun getDevices(): Array<Device> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, consuming application is expected to cache this, right? Afaik every call to getDevices will hit the network currently. That's probably fine for the first iteration. Keeping a cache of devices updated will be an annoyance...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW it seems reasonable for this method to grow its own internal caching over time, especially since we're supposed to receive push notifications that tell us when to invalidate that cache.

val devicesBuffer = rustCall { e ->
LibFxAFFI.INSTANCE.fxa_get_devices(this.handle.get(), e)
}
try {
val devices = MsgTypes.Devices.parseFrom(devicesBuffer.asCodedInputStream()!!)
return Device.fromCollectionMessage(devices)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(devicesBuffer)
}
}

/**
* Retrieves any pending commands for the current device.
* This should be called semi-regularly as the main method of commands delivery (push)
* can sometimes be unreliable on mobile devices.
* If a persist callback is set and the host application failed to process the
* returned account events, they will never be seen again.
*
* This performs network requests, and should not be used on the main thread.
*
* @return A collection of [AccountEvent] that should be handled by the caller.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should the caller look for documentation on the possible [AccountEvent] types and what actions it should take in response to them? Is this a doc that needs to exist somewhere in FxA?

*/
fun pollDeviceCommands(): Array<AccountEvent> {
val eventsBuffer = rustCall { e ->
LibFxAFFI.INSTANCE.fxa_poll_device_commands(this.handle.get(), e)
}
this.tryPersistState()
try {
val events = MsgTypes.AccountEvents.parseFrom(eventsBuffer.asCodedInputStream()!!)
return AccountEvent.fromCollectionMessage(events)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
}
}

/**
* Handle any incoming push message payload coming from the Firefox Accounts
* servers that has been decrypted and authenticated by the Push crate.
*
* This performs network requests, and should not be used on the main thread.
*
* @return A collection of [AccountEvent] that should be handled by the caller.
*/
fun handlePushMessage(payload: String): Array<AccountEvent> {
val eventsBuffer = rustCall { e ->
LibFxAFFI.INSTANCE.fxa_handle_push_message(this.handle.get(), payload, e)
}
this.tryPersistState()
try {
val events = MsgTypes.AccountEvents.parseFrom(eventsBuffer.asCodedInputStream()!!)
return AccountEvent.fromCollectionMessage(events)
} finally {
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
}
}

/**
* Ensure the current device is registered with the specified name and device type, with
* the required capabilities (at this time only Send Tab).
* This method should be called once per "device lifetime".
*
* This performs network requests, and should not be used on the main thread.
*/
fun initializeDevice(name: String, deviceType: Device.Type, supportedCapabilities: Set<Device.Capability>) {
val capabilitiesBuilder = MsgTypes.Capabilities.newBuilder()
supportedCapabilities.forEach {
when (it) {
Device.Capability.SEND_TAB -> capabilitiesBuilder.addCapability(MsgTypes.Device.Capability.SEND_TAB)
}.exhaustive
}
val buf = capabilitiesBuilder.build()
val (nioBuf, len) = buf.toNioDirectBuffer()
rustCall { e ->
val ptr = Native.getDirectBufferPointer(nioBuf)
LibFxAFFI.INSTANCE.fxa_initialize_device(this.handle.get(), name, deviceType.toNumber(), ptr, len, e)
}
this.tryPersistState()
}

/**
* Ensure that the supported capabilities described earlier in `initializeDevice` are A-OK.
* As for now there's only the Send Tab capability, so we ensure the command is registered with the server.
* This method should be called at least every time the sync keys change (because Send Tab relies on them).
*
* This performs network requests, and should not be used on the main thread.
*/
fun ensureCapabilities() {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_ensure_capabilities(this.handle.get(), e)
}
this.tryPersistState()
}

/**
* Send a single tab to another device identified by its device ID.
*
* This performs network requests, and should not be used on the main thread.
*
* @param targetDeviceId The target Device ID
* @param title The document title of the tab being sent
* @param url The url of the tab being sent
*/
fun sendSingleTab(targetDeviceId: String, title: String, url: String) {
rustCall { e ->
LibFxAFFI.INSTANCE.fxa_send_tab(this.handle.get(), targetDeviceId, title, url, e)
}
}

@Synchronized
override fun close() {
val handle = this.handle.getAndSet(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@file:Suppress("MaxLineLength")
/* 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/. */
Expand All @@ -11,7 +12,7 @@ import com.sun.jna.Pointer
import java.lang.reflect.Proxy
import mozilla.appservices.support.RustBuffer

@Suppress("FunctionNaming", "FunctionParameterNaming", "TooGenericExceptionThrown")
@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown")
internal interface LibFxAFFI : Library {
companion object {
private val JNA_LIBRARY_NAME = {
Expand Down Expand Up @@ -76,6 +77,29 @@ internal interface LibFxAFFI : Library {
fun fxa_complete_oauth_flow(fxa: FxaHandle, code: String, state: String, e: RustError.ByReference)
fun fxa_get_access_token(fxa: FxaHandle, scope: String, e: RustError.ByReference): RustBuffer.ByValue

fun fxa_set_push_subscription(
fxa: FxaHandle,
endpoint: String,
publicKey: String,
authKey: String,
e: RustError.ByReference
)
fun fxa_set_device_name(fxa: FxaHandle, displayName: String, e: RustError.ByReference)
fun fxa_get_devices(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_poll_device_commands(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
fun fxa_handle_push_message(fxa: FxaHandle, jsonPayload: String, e: RustError.ByReference): RustBuffer.ByValue

fun fxa_initialize_device(
fxa: FxaHandle,
name: String,
type: Int,
capabilities_data: Pointer,
capabilities_len: Int,
e: RustError.ByReference
)
fun fxa_ensure_capabilities(fxa: FxaHandle, e: RustError.ByReference)
fun fxa_send_tab(fxa: FxaHandle, targetDeviceId: String, title: String, url: String, e: RustError.ByReference)

fun fxa_str_free(string: Pointer)
fun fxa_bytebuffer_free(buffer: RustBuffer.ByValue)
fun fxa_free(fxa: FxaHandle, err: RustError.ByReference)
Expand Down
Loading