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

    View full-size slide

  2. C R O S S D E V I C E S D K

    View full-size slide

  3. C R O S S D E V I C E S D K
    Google Play Services
    BT/BLE WiFi UWB

    View full-size slide

  4. 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

    View full-size slide

  5. 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!

    View full-size slide

  6. 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

    View full-size slide

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

    View full-size slide

  8. 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"
    }


    )


    }

    View full-size slide

  9. 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

    View full-size slide

  10. 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
    }


    }

    View full-size slide

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

    View full-size slide

  12. 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))

    View full-size slide

  13. 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
    }


    }


    )


    }

    View full-size slide

  14. S E S S I O N S A P I

    View full-size slide

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

    View full-size slide

  16. 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

    View full-size slide

  17. 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

    View full-size slide

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

    View full-size slide

  19. 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

    View full-size slide

  20. 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

    View full-size slide

  21. 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

    View full-size slide

  22. 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

    View full-size slide

  23. 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

    View full-size slide

  24. S E S S I O N S A P I
    S H A R E

    View full-size slide

  25. 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

    View full-size slide

  26. 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

    View full-size slide

  27. 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

    View full-size slide

  28. 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

    View full-size slide

  29. 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

    View full-size slide

  30. 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

    View full-size slide

  31. 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

    View full-size slide

  32. 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

    View full-size slide

  33. https://d.android.com/guide/topics/connectivity/cross-device-sdk/overview
    D O C S
    https://github.com/android/connectivity-samples/tree/main/CrossDeviceRockPaperScissorsKotlin
    S A M P L E S
    https://github.com/google/cross-device-sdk
    S D K S O U R C E S

    View full-size slide

  34. Daniele Bonaldo
    @danybony_
    www.danielebonaldo.com
    Thank you!

    View full-size slide