-
Notifications
You must be signed in to change notification settings - Fork 256
Send Tab #676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Send Tab #676
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yup my rationale is that the clients don't need the
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 |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
| /** | ||
|
|
@@ -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) | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where should the caller look for documentation on the possible |
||
| */ | ||
| fun pollDeviceCommands(): Array<AccountEvent> { | ||
| val eventsBuffer = rustCall { e -> | ||
| LibFxAFFI.INSTANCE.fxa_poll_device_commands(this.handle.get(), e) | ||
| } | ||
| this.tryPersistState() | ||
eoger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.