Upgrade to Pro — share decks privately, control downloads, hide ads and more …

When one device is not enough

danybony
October 22, 2022

When one device is not enough

As developers we know how to build apps that are flexible enough to run on different types of devices and properly display information on different screen sizes. But what if a user wants to easily discover nearby devices and continue interacting with your app in a seamless way?
In this talk we’ll explore the new Cross Device SDK, which allows us to build multi-device experiences enabling the users to discover nearby devices, establish secure connections, and move experiences between devices.

Presented at Droidcon Egypt 2022.

danybony

October 22, 2022
Tweet

More Decks by danybony

Other Decks in Programming

Transcript

  1. W H E N O N E D E V

    I C E I S N O T E N O U G H D A N I E L E B O N A L D O
  2. C R O S S D E V I C

    E S D K Google Play Services BT/BLE WiFi UWB
  3. S I M I L A R M U LT

    I - D E V I C E E X P E R I E N C E S B L O C K S T O R E M E D I A S E S S I O N S C O M PA N I O N D E V I C E M A N A G E R C A S T S D K
  4. D E V E L O P E R P

    R E V I E W ⚠ L I M I TAT I O N S Phone and tablet only Android only 2 devices max Non- fi nal API Not ready for production !!1!
  5. D E V I C E D I S C

    O V E RY A P I S E C U R E C O N N E C T I O N A P I S E S S I O N S A P I C R O S S D E V I C E S D K
  6. D E V I C E D I S C

    O V E RY A P I
  7. D E V I C E D I S C

    O V E RY A P I val devicePickerLauncher = Discovery.create(context) .registerForResult(context as ActivityResultCaller) { participants -> participants.forEach { // open a connection } } coroutineScope.launch { devicePickerLauncher.launchDevicePicker( filters, startComponentRequest { action = "com.example.PARTICIPANT_WAKEUP_ACTION" reason = "Message in the device picker" } ) }
  8. D E V I C E D I S C

    O V E RY A P I D E V I C E F I LT E R S trustRelationshipFilter UNSPECIFIED MY_DEVICES_ONLY featureFilter Not in developer preview yet <uses-feature>
  9. D E V I C E D I S C

    O V E RY A P I override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //... handleIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) } private fun handleIntent(intent: Intent) { if (intent.action == "com.example.PARTICIPANT_WAKEUP_ACTION") { val participant = Discovery.create(context) .getParticipantFromIntent(intent) // accept the connection } }
  10. S E C U R E C O N N

    E C T I O N A P I
  11. S E C U R E C O N N

    E C T I O N A P I coroutineScope.launch { participant.openConnection("channel_name") .onFailure { // handle connection errors } .getOrNull() ?.let { remoteConnection -> connectedDevice = remoteConnection connectedDevice.registerReceiver(…) } } connectedDevice.send("any message".toByteArray(UTF_8))
  12. S E C U R E C O N N

    E C T I O N A P I suspend fun acceptIncomingConnection(participant: Participant) { val connection = participant.acceptConnection("channel_name").getOrThrow() connection.registerReceiver( object : ConnectionReceiver { override fun onMessageReceived( remoteConnection: RemoteConnection, payload: ByteArray ) { // handle received message } override fun onConnectionClosed( remoteConnection: RemoteConnection, error: Throwable?, reason: String? ) { // handle connection closure } } ) }
  13. S E S S I O N S A P

    I T R A N S F E R
  14. S E S S I O N S E S

    S I O N S A P I T R A N S F E R
  15. S E S S I O N S E S

    S I O N S A P I T R A N S F E R
  16. S E S S I O N S A P

    I T R A N S F E R
  17. val sessions = Sessions.create(context) val sessionId = sessions.createSession( ApplicationSessionTag("session_tag") )

    val originatingSession = sessions.transferSession( sessionId, StartComponentRequest.Builder() .setAction("com.example.SESSION_TRANSFER_ACTION") .setReason("Transfer reason here") .build(), filters, originatingSessionStateCallback ) originatingSessionStateCallback S E S S I O N S A P I T R A N S F E R
  18. private val originatingSessionStateCallback: OriginatingSessionStateCallback = object : OriginatingSessionStateCallback { override

    fun onConnected(sessionId: SessionId) { val startupRemoteConnection = originatingSession.getStartupRemoteConnection() lifecycleScope.launch { startupRemoteConnection.send(/* ByteArray representation of the session */) startupRemoteConnection.registerReceiver( object : SessionConnectionReceiver { override fun onMessageReceived( participant: SessionParticipant, payload: ByteArray ) { // We can wait for a confirmation from the receiver } } ) } } override fun onSessionTransferred(sessionId: SessionId) { // Update the originating device UI } override fun onTransferFailure(sessionId: SessionId, exception: SessionException) { // Handle error } } originatingSessionStateCallback S E S S I O N S A P I T R A N S F E R
  19. private val originatingSessionStateCallback: OriginatingSessionStateCallback = object : OriginatingSessionStateCallback { override

    fun onConnected(sessionId: SessionId) { val startupRemoteConnection = originatingSession.getStartupRemoteConnection() lifecycleScope.launch { startupRemoteConnection.send(/* ByteArray representation of the session */) startupRemoteConnection.registerReceiver( object : SessionConnectionReceiver { override fun onMessageReceived( participant: SessionParticipant, payload: ByteArray ) { // We can wait for a confirmation from the receiver } } ) } } override fun onSessionTransferred(sessionId: SessionId) { // Update the originating device UI } override fun onTransferFailure(sessionId: SessionId, exception: SessionException) { // Handle error } } S E S S I O N S A P I T R A N S F E R
  20. private fun acceptSession(intent: Intent) { val sessions = Sessions.create(context) lifecycleScope.launch

    { sessions.getReceivingSession(intent, receivingSessionStateCallback) .getStartupRemoteConnection() .registerReceiver( object : SessionConnectionReceiver { override fun onMessageReceived( participant: SessionParticipant, payload: ByteArray ) { // Update the receiver UI } } ) } } receivingSessionStateCallback S E S S I O N S A P I T R A N S F E R
  21. private val receivingSessionStateCallback: ReceivingSessionStateCallback = object : ReceivingSessionStateCallback { override

    fun onTransferFailure( sessionId: SessionId, exception: SessionException ) { // Handle session sharing error } } receivingSessionStateCallback S E S S I O N S A P I T R A N S F E R
  22. S E S S I O N S A P

    I S H A R E
  23. P R I M A RY S E S S

    I O N S E S S I O N S A P I S H A R E S E C O N D A RY S E S S I O N S E C O N D A RY S E S S I O N
  24. S E C O N D A RY S E

    S S I O N S E C O N D A RY S E S S I O N P R I M A RY S E S S I O N S E S S I O N S A P I S H A R E
  25. suspend fun shareSession() { val sessions = Sessions.create(context) val sessionId

    = sessions.createSession( ApplicationSessionTag("share_session_tag") ) activePrimarySession = sessions.shareSession( sessionId, StartComponentRequest.Builder() .setAction("com.example.SESSION_SHARE_ACTION") .setReason("Share reason here") .build(), deviceFilters, shareSessionStateCallback ) } S E S S I O N S A P I S H A R E
  26. suspend fun shareSession() { val sessions = Sessions.create(context) val sessionId

    = sessions.createSession( ApplicationSessionTag("share_session_tag") ) activePrimarySession = sessions.shareSession( sessionId, StartComponentRequest.Builder() .setAction("com.example.SESSION_SHARE_ACTION") .setReason("Share reason here") .build(), deviceFilters, shareSessionStateCallback ) } S E S S I O N S A P I S H A R E
  27. private val shareSessionStateCallback = object : PrimarySessionStateCallback { override fun

    onShareInitiated(sessionId: SessionId, numPotentialParticipants: Int) { // Custom logic here for when n devices can potentially join } override fun onParticipantJoined(sessionId: SessionId, participant: SessionParticipant) { // Handle participant joined } override fun onParticipantDeparted(sessionId: SessionId, participant: SessionParticipant) { // Handle participant left } override fun onPrimarySessionCleanup(sessionId: SessionId) { // Remove session references } override fun onShareFailureWithParticipant( sessionId: SessionId, exception: SessionException, participant: SessionParticipant ) { // Handle error } } S E S S I O N S A P I S H A R E
  28. override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) val sessions = Sessions.create(context)

    lifecycleScope.launchWhenResumed { val secondarySession = sessions.getSecondarySession( intent, secondaryShareSessionStateCallback ) // Keep reference to the connection to send data later val remoteConnection = secondarySession.getDefaultRemoteConnection() remoteConnection.registerReceiver(object : SessionConnectionReceiver { override fun onMessageReceived( participant: SessionParticipant, payload: ByteArray ) { // Handle message received } }) } } S E S S I O N S A P I S H A R E
  29. private val secondaryShareSessionStateCallback = object : SecondarySessionStateCallback { override fun

    onSecondarySessionCleanup(sessionId: SessionId) { // Remove session references } } S E S S I O N S A P I S H A R E
  30. P E R S O N A L U S

    E C A S E S C O M M U N A L U S E C A S E S S T I L L I N P R E V I E W