API Sessions

Criado com base nas APIs Device Discovery e Secure Connection, a API Sessions fornece uma abstração avançada para criar experiências integradas entre dispositivos. Uma sessão representa uma experiência do usuário de aplicativo que pode ser transferida e compartilhada entre dispositivos.

A API Sessions também foi criada com base na noção de experiências pessoais e comunitárias representadas por casos de uso de transferência de sessão e de compartilhamento de sessão, respectivamente. O diagrama a seguir ilustra as sessões de forma geral:

Diagrama de sessões.
Figura 1: diagrama de sessões.

Criar e transferir uma sessão

A API Sessions diferencia o dispositivo de origem do dispositivo de recebimento. O dispositivo de origem cria a sessão e procura um dispositivo capaz de processar a sessão. O usuário do dispositivo de origem seleciona um dispositivo na lista fornecida pela caixa de diálogo do sistema. Depois que o usuário seleciona o dispositivo receptor, a sessão de origem é transferida e removida do dispositivo.

Para transferir a sessão, primeiro você precisa criá-la usando os seguintes parâmetros:

  • Uma tag de sessão do aplicativo: um identificador que permite diferenciar várias sessões no seu app.

Em seguida, inicie a transferência usando os seguintes parâmetros:

  • Um DeviceFilter para filtrar os dispositivos que podem gerenciar a sessão.
  • Um objeto de callback que implementa OriginatingSessionStateCallback

No dispositivo de origem, crie uma sessão usando o exemplo abaixo:

Kotlin

private val HELLO_WORLD_TRANSFER_ACTION = "hello_world_transfer"
private lateinit var originatingSession: OriginatingSession
private lateinit var sessions: Sessions

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sessions = Sessions.create(context = this)
}

suspend fun transferSession() {
  val sessionId =
    sessions.createSession(
      ApplicationSessionTag("hello_world_transfer"),
    )
  originatingSession =
    sessions.transferSession(
      sessionId,
      StartComponentRequest.Builder()
        .setAction(HELLO_WORLD_TRANSFER_ACTION)
        .setReason("Transfer reason here")
        .build(),
      emptyList(),
      HelloWorldTransferSessionStateCallback()
    )
}

Java

private static final String HELLO_WORLD_TRANSFER_ACTION = "hello_world_transfer";
private OriginatingSession originatingSession;
private Sessions sessions;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  sessions = Sessions.create(/* context= */ this);
}

public void transferSession() {
  SessionId sessionId = sessions.createSession(new ApplicationSessionTag("hello_world_transfer"));
  ListenableFuture<OriginatingSession> originatingSessionFuture =
      sessions.transferSessionFuture(
          sessionId,
          new StartComponentRequest.Builder()
              .setAction(HELLO_WORLD_TRANSFER_ACTION)
              .setReason("Transfer reason here")
              .build(),
          Collections.emptyList(),
          new HelloWorldTransferSessionStateCallback());
  Futures.addCallback(
      originatingSessionFuture,
      new FutureCallback<>() {
        @Override
        public void onSuccess(OriginatingSession result) {
          // Do nothing, handled in HelloWorldTransferSessionStateCallback
          originatingSession = result;
        }

        @Override
        public void onFailure(Throwable t) {
          Log.d(TAG, "onFailure called for transferSessionFuture", t);
        }
      },
      mainExecutor);
}

Em seguida, defina o callback da sessão no dispositivo de origem:

Kotlin

private inner class HelloWorldTransferSessionStateCallback : OriginatingSessionStateCallback {
  override fun onConnected(sessionId: SessionId) {
    val startupRemoteConnection = originatingSession.getStartupRemoteConnection()
    lifecycleScope.launchWhenResumed {
      startupRemoteConnection.send("hello, world".toByteArray(UTF_8))
      startupRemoteConnection.registerReceiver(
        object : SessionConnectionReceiver {
          override fun onMessageReceived(participant: SessionParticipant, payload: ByteArray) {
            val ok = payload.contentEquals("ok".toByteArray(UTF_8))
            Log.d(TAG, "Session transfer initialized. ok=$ok")
          }
        }
      )
    }
  }

  override fun onSessionTransferred(sessionId: SessionId) {
    Log.d(TAG, "Transfer done.")
  }

  override fun onTransferFailure(sessionId: SessionId, exception: SessionException) {
    // Handle error
  }
}

Java

private class HelloWorldTransferSessionStateCallback implements OriginatingSessionStateCallback {
  @Override
  public void onConnected(SessionId sessionId) {
    SessionRemoteConnection startupRemoteConnection =
        originatingSession.getStartupRemoteConnection();
    Futures.addCallback(
        startupRemoteConnection.sendFuture("hello, world".getBytes()),
        new FutureCallback<>() {
          @Override
          public void onSuccess(Void result) {
            Log.d(TAG, "Successfully sent initialization message");
          }

          @Override
          public void onFailure(Throwable t) {
            Log.d(TAG, "Failed to send initialization message", t);
          }
        },
        mainExecutor);
  }

  @Override
  public void onSessionTransferred(SessionId sessionId) {
    Log.d(TAG, "Transfer done.");
  }

  @Override
  public void onTransferFailure(SessionId sessionId, SessionException exception) {
    // Handle error
  }
}

Depois que a transferência da sessão é iniciada, o dispositivo receptor recebe um callback no método onNewIntent(intent: Intent). Os dados da intent contêm tudo o necessário para transferir a sessão.

Para concluir a transferência no dispositivo de destino, faça o seguinte:

Kotlin

private lateinit var sessions: Sessions

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sessions = Sessions.create(context = this)
}

override fun onNewIntent(intent: Intent) {
  super.onNewIntent(intent)
  lifecycleScope.launchWhenResumed {
    val receivingSession =
      sessions.getReceivingSession(intent, HelloWorldReceivingSessionStateCallback())

    // Get info from receiving device and init.
    val startupRemoteConnection = receivingSession.getStartupRemoteConnection()

    startupRemoteConnection.registerReceiver(
      object : SessionConnectionReceiver {
        override fun onMessageReceived(participant: SessionParticipant, payload: ByteArray) {
          lifecycleScope.launchWhenResumed {
            val transferInfo = String(payload)
            startupRemoteConnection.send("ok".toByteArray(UTF_8))
            // Complete transfer.
            Log.d(TAG, "Transfer info: " + transferInfo)
            receivingSession.onComplete()
          }
        }
      }
    )
  }
}

private inner class HelloWorldReceivingSessionStateCallback : ReceivingSessionStateCallback {
  override fun onTransferFailure(sessionId: SessionId, exception: SessionException) {
    // Handle error
  }
}

Java

private Sessions sessions;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  sessions = Sessions.create(/* context= */ this);
}

@Override
public void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  ListenableFuture<ReceivingSession> receivingSessionFuture =
      sessions.getReceivingSessionFuture(intent, new HelloWorldReceivingSessionStateCallback());
  ListenableFuture<Void> registerReceiverFuture =
      Futures.transform(
          receivingSessionFuture,
          receivingSession -> {
            SessionRemoteConnection startupRemoteConnection =
                receivingSession.getStartupRemoteConnection();
            SessionConnectionReceiver receiver =
                (participant, payload) -> {
                  Log.d(
                      TAG,
                      "Successfully received initialization message of size: " + payload.length);
                  applicationInitialization(receivingSession, payload);
                };
            startupRemoteConnection.registerReceiver(receiver);
            return null;
          },
          mainExecutor);
  Futures.addCallback(
      registerReceiverFuture,
      new FutureCallback<Void>() {
        @Override
        public void onSuccess(Void unused) {
          Log.d(TAG, "Connection receiver registerd successfully");
        }

        @Override
        public void onFailure(Throwable t) {
          Log.w(TAG, "Failed to register connection receiver", t);
        }
      },
      mainExecutor);
}

private void applicationInitialization(ReceivingSession receivingSession, byte[] initMessage) {
  ListenableFuture<SessionId> disconnectFuture =
      Futures.transform(
          receivingSession.onCompleteFuture(),
          sessionId -> {
            Log.d(TAG, "Succeeded to complete receive transfer for: " + sessionId);
            return sessionId;
          },
          mainExecutor);
  Futures.addCallback(
      disconnectFuture,
      new FutureCallback<SessionId>() {
        @Override
        public void onSuccess(SessionId result) {
          Log.d(TAG, "Succeeded to remove the old session: " + result);
        }

        @Override
        public void onFailure(Throwable t) {
          Log.d(TAG, "Failed to remove the old session, which is now orphaned", t);
        }
      },
      mainExecutor);
}

private static class HelloWorldReceivingSessionStateCallback
    implements ReceivingSessionStateCallback {
  @Override
  public void onTransferFailure(SessionId sessionId, SessionException exception) {
    // Handle error
  }
}

Agora o dispositivo receptor pode prosseguir com a experiência do usuário.

Compartilhar uma sessão

Ao compartilhar uma sessão, você pode convidar outras pessoas ao seu redor para participar de uma experiência de grupo, por exemplo:

  • Compartilhe um local no mapa como passageiro diretamente com o carro de um amigo.
  • Compartilhe seu trajeto de bicicleta aos domingos com outras pessoas.
  • Colete itens para um pedido de comida em grupo sem precisar passar o smartphone.
  • Faça um voto em grupo para o próximo programa de TV para assistirem juntos.

Quando um usuário decide compartilhar uma sessão com outro dispositivo, o dispositivo de origem pesquisa e apresenta dispositivos capazes de entrar na sessão, e o usuário seleciona os dispositivos de destino. O aplicativo solicita que o usuário em um dispositivo de recebimento participe da sessão pelo dispositivo de origem. Um dispositivo recebedor recebe uma sessão secundária para interagir com a sessão no dispositivo de origem. Os aplicativos também podem adicionar outros participantes à sessão compartilhada em andamento.

O processo de compartilhamento de uma sessão é semelhante à transferência de uma sessão, mas, em vez de chamar transferSession, chame shareSession. As outras diferenças estão nos métodos de callback do estado da sessão.

Kotlin

// Originating side.
private val HELLO_WORLD_SHARE_ACTION = "hello_world_share"
private var activePrimarySession: PrimarySession? = null
private lateinit var sessions: Sessions

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  sessions = Sessions.create(context = this)
}

suspend fun shareSession() {
  val sessionId = sessions.createSession(ApplicationSessionTag("hello_world_share"))
  activePrimarySession =
    sessions.shareSession(
      sessionId,
      StartComponentRequest.Builder()
        .setAction(HELLO_WORLD_SHARE_ACTION)
        .setReason("Share reason here")
        .build(),
      emptyList(),
      HelloWorldShareSessionStateCallback(),
    )
}

private inner class HelloWorldShareSessionStateCallback : PrimarySessionStateCallback {

  override fun onShareInitiated(sessionId: SessionId, numPotentialParticipants: Int) {
    // Custom logic here for when n devices can potentially join.
    // e.g. if there were 0, cancel/error if desired,
    //      if non-0 maybe spin until numPotentialParticipants join etc.
  }

  override fun onParticipantJoined(sessionId: SessionId, participant: SessionParticipant) {
    // Custom join logic here
    lifecycleScope.launchWhenResumed {
      // Example logic: send only to the participant who just joined.
      val connection =
        checkNotNull(activePrimarySession).getSecondaryRemoteConnectionForParticipant(participant)
      connection.send("Initing hello, world.".toByteArray(UTF_8))
      connection.registerReceiver(
        object : SessionConnectionReceiver {
          override fun onMessageReceived(participant: SessionParticipant, payload: ByteArray) {
            val ok = payload.contentEquals("ok".toByteArray(UTF_8))
            Log.d(TAG, "Session share initialized. ok=$ok")

            // Example logic: broadcast to all participants, including the one
            // that just joined.
            lifecycleScope.launchWhenResumed {
              checkNotNull(activePrimarySession)
                .broadcastToSecondaries("hello, all.".toByteArray(UTF_8))
            }
          }
        }
      )
    }
  }

  override fun onParticipantDeparted(sessionId: SessionId, participant: SessionParticipant) {
    // Custom leave logic here.
  }

  override fun onPrimarySessionCleanup(sessionId: SessionId) {
    // Custom cleanup logic here.
    activePrimarySession = null
  }

  override fun onShareFailureWithParticipant(
    sessionId: SessionId,
    exception: SessionException,
    participant: SessionParticipant
  ) {
    // Handle error
  }
}

Java

// Originating side
private static final String HELLO_WORLD_SHARE_ACTION = "hello_world_share";
@Nullable private PrimarySession activePrimarySession = null;
private Sessions sessions;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  sessions = Sessions.create(/* context= */ this);
}

private void shareSession() {
  SessionId sessionId = sessions.createSession(new ApplicationSessionTag("hello_world_share"));
  ListenableFuture<PrimarySession> shareSessionFuture =
      sessions.shareSessionFuture(
          sessionId,
          new StartComponentRequest.Builder()
              .setAction(HELLO_WORLD_SHARE_ACTION)
              .setReason("Share reason here")
              .build(),
          Collections.emptyList(),
          new HelloWorldShareSessionStateCallback());
  Futures.addCallback(
      shareSessionFuture,
      new FutureCallback<PrimarySession>() {
        @Override
        public void onSuccess(PrimarySession primarySession) {
          activePrimarySession = primarySession;
        }

        @Override
        public void onFailure(Throwable t) {
          Log.d(TAG, "Failed to share session", t);
        }
      },
      mainExecutor);
}

private class HelloWorldShareSessionStateCallback implements PrimarySessionStateCallback {
  @Override
  public void onShareInitiated(SessionId sessionId, int numPotentialParticipants) {
    // Custom logic here for when n devices can potentially join.
    // e.g. if there were 0, cancel/error if desired,
    //      if non-0 maybe spin until numPotentialParticipants join etc.
  }

  @Override
  public void onParticipantJoined(SessionId sessionId, SessionParticipant participant) {
    PrimarySession joinedSession = activePrimarySession;
    if (joinedSession == null) {
      return;
    }
    SessionRemoteConnection connection =
        joinedSession.getSecondaryRemoteConnectionForParticipant(participant);
    Futures.addCallback(
        connection.sendFuture("Initiating hello, world.".getBytes()),
        new FutureCallback<Void>() {
          @Override
          public void onSuccess(Void result) {
            // Send successful.
          }

          @Override
          public void onFailure(Throwable t) {
            // Failed to send.
          }
        },
        mainExecutor);
    connection.registerReceiver(
        new SessionConnectionReceiver() {
          @Override
          public void onMessageReceived(SessionParticipant participant, byte[] payload) {
            boolean ok = new String(payload, UTF_8).equals("ok");
            Log.d(TAG, "Session share initialized. ok=" + ok);

            // Example logic: broadcast to all participants, including the one
            // that just joined.
            Futures.addCallback(
                joinedSession.broadcastToSecondariesFuture("hello, all.".getBytes()),
                new FutureCallback<Void>() {
                  @Override
                  public void onSuccess(Void result) {
                    // Broadcast successful.
                  }

                  @Override
                  public void onFailure(Throwable t) {
                    // Failed to broadcast hello world.
                  }
                },
                mainExecutor);
          }
        });
  }

  @Override
  public void onParticipantDeparted(SessionId sessionId, SessionParticipant participant) {
    // Custom leave logic here.
  }

  @Override
  public void onPrimarySessionCleanup(SessionId sessionId) {
    // Custom cleanup logic here.
    activePrimarySession = null;
  }

  @Override
  public void onShareFailureWithParticipant(
      SessionId sessionId, SessionException exception, SessionParticipant participant) {
    // Custom error handling logic here.
  }
}

No lado do destinatário:

Kotlin

// Receiving side.

override fun onNewIntent(intent: Intent) {
  super.onNewIntent(intent)
  lifecycleScope.launchWhenResumed {
    val secondarySession =
      sessions.getSecondarySession(intent, HelloWorldSecondaryShareSessionStateCallback())
    val remoteConnection = secondarySession.getDefaultRemoteConnection()

    remoteConnection.registerReceiver(
      object : SessionConnectionReceiver {
        override fun onMessageReceived(participant: SessionParticipant, payload: ByteArray) {
          Log.d(TAG, "Payload received: ${String(payload)}")
        }
      }
    )
  }
}

private inner class HelloWorldSecondaryShareSessionStateCallback : SecondarySessionStateCallback {
  override fun onSecondarySessionCleanup(sessionId: SessionId) {
    // Custom cleanup logic here.
  }
}

Java

// Receiving side.
@Override
public void onNewIntent(Intent intent) {
  super.onNewIntent(intent);
  sessions = Sessions.create(this);
  ListenableFuture<SecondarySession> secondarySessionFuture =
      sessions.getSecondarySessionFuture(
          intent, new HelloWorldSecondaryShareSessionStateCallback());
  Futures.addCallback(
      secondarySessionFuture,
      new FutureCallback<SecondarySession>() {
        @Override
        public void onSuccess(SecondarySession secondarySession) {
          SessionRemoteConnection remoteConnection =
              secondarySession.getDefaultRemoteConnection();
          remoteConnection.registerReceiver(
              new SessionConnectionReceiver() {
                @Override
                public void onMessageReceived(SessionParticipant participant, byte[] payload) {
                  Log.d(TAG, "Payload received: " + new String(payload, UTF_8));
                }
              });
        }

        @Override
        public void onFailure(Throwable t) {
          // Handle error.
        }
      },
      mainExecutor);
}

private static class HelloWorldSecondaryShareSessionStateCallback
    implements SecondarySessionStateCallback {
  @Override
  public void onSecondarySessionCleanup(SessionId sessionId) {
    // Custom cleanup logic here.
  }
}