Communication bidirectionnelle sans Internet

1. Avant de commencer

Et si vous pouviez utiliser votre appareil mobile pour collaborer sur des projets de groupe, partager des vidéos, diffuser du contenu en streaming ou jouer à un jeu multijoueur, même sans accès à Internet ? Sachez que cela est possible. Dans cet atelier de programmation, vous découvrirez comment.

Pour simplifier les choses, nous créerons un jeu multijoueur de type "pierre-papier-ciseaux" qui fonctionne sans Internet. Cet atelier de programmation explique comment utiliser l'API Nearby Connections, qui fait partie des services Google Play, pour permettre aux utilisateurs de communiquer entre eux en fonction de leur proximité physique. Les utilisateurs doivent se trouver dans un rayon d'environ 100 mètres. Le type et la quantité de données qu'ils peuvent partager sont illimités, même sans connexion Internet. Ils peuvent regarder des vidéos en streaming, envoyer et recevoir des messages vocaux, envoyer des SMS et plus encore.

Conditions préalables

  • Vous disposez de connaissances de base en développement avec Kotlin et Android.
  • Vous savez créer et exécuter des applications dans Android Studio.
  • Vous avez au moins deux appareils Android pour exécuter et tester le code.
  • Vous exécutez l'API Android niveau 16 ou supérieur.
  • Vous avez installé les services Google Play.
  • Vous avez installé la dernière version d'Android Studio.

Points abordés

  • Ajouter la bibliothèque des services Google Play Nearby Connections à votre application
  • Indiquer que vous souhaitez communiquer avec les appareils à proximité
  • Découvrir les appareils à proximité qui vous intéressent
  • Communiquer avec des appareils connectés
  • Bonnes pratiques concernant la confidentialité et la protection des données

Objectifs de l'atelier

Cet atelier de programmation explique comment créer une application d'activité unique qui permet à un utilisateur de trouver des adversaires pour jouer à un jeu de pierre-papier-ciseaux. L'application comporte les éléments d'interface utilisateur suivants :

  1. Un bouton permettant de rechercher des adversaires
  2. Une manette de jeu avec trois boutons permettant aux utilisateurs de choisir ROCK (PIERRE), PAPER (PAPIER) ou SCISSORS (CISEAUX)
  3. Des vues TextView pour l'affichage des scores
  4. Une vue TextView pour l'affichage de l'état

625eeebfad3b195a.png

Figure 1

2. Créer un projet Android Studio

  1. Lancez un nouveau projet Android Studio.
  2. Sélectionnez Empty Activity (Activité vide).

f2936f15aa940a21.png

  1. Nommez le projet Rock Paper Scissors (Pierre, papier, ciseaux) et définissez le langage sur Kotlin.

1ea410364fbdfc31.png

3. Configurer le code

  1. Ajoutez la dernière version de la dépendance Nearby dans le fichier build.gradle au niveau de l'application. Cette action permettra à votre application d'utiliser l'API Nearby Connections pour indiquer que vous souhaitez indiquer votre présence, détecter les appareils à proximité et communiquer avec eux.
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
  1. Définissez l'option de compilation viewBinding sur true dans le bloc android pour activer l'option View Binding (Liaison de vue). De cette manière, vous n'aurez pas à utiliser findViewById pour interagir avec les vues.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Cliquez sur Sync Now (Synchroniser) ou sur le bouton en forme de marteau vert pour qu'Android Studio tienne compte de ces modifications Gradle.

57995716c771d511.png

  1. Nous utiliserons des drawables vectoriels pour nos images de pierre, de papier et de ciseaux. Ajoutez les trois fichiers XML suivants dans le répertoire res/drawable.

res/drawables/rock.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportHeight="48.0"
   android:viewportWidth="48.0"
   android:width="24dp">
 <path
     android:fillColor="#ffffff"
     android:pathData="M28,12l-7.5,10 5.7,7.6L23,32c-3.38,-4.5 -9,-12 -9,-12L2,36h44L28,12z"/>
</vector>

res/drawables/paper.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportHeight="48.0"
   android:viewportWidth="48.0"
   android:width="24dp">
 <path
     android:fillColor="#ffffff"
     android:pathData="M28,4L12,4C9.79,4 8.02,5.79 8.02,8L8,40c0,2.21 1.77,4 3.98,4L36,44c2.21,0 4,-1.79 4,-4L40,16L28,4zM26,18L26,7l11,11L26,18z"/>
</vector>

res/drawables/scissors.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
   android:width="24dp"
   android:height="24dp"
   android:tintMode="multiply"
   android:viewportWidth="48.0"
   android:viewportHeight="48.0">
   <path
       android:fillColor="#ffffff"
       android:pathData="M19.28,15.28c0.45,-1 0.72,-2.11 0.72,-3.28 0,-4.42 -3.58,-8 -8,-8s-8,3.58 -8,8 3.58,8 8,8c1.17,0 2.28,-0.27 3.28,-0.72L20,24l-4.72,4.72c-1,-0.45 -2.11,-0.72 -3.28,-0.72 -4.42,0 -8,3.58 -8,8s3.58,8 8,8 8,-3.58 8,-8c0,-1.17 -0.27,-2.28 -0.72,-3.28L24,28l14,14h6v-2L19.28,15.28zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM12,40c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4zM24,25c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM38,6L26,18l4,4L44,8L44,6z" />
</vector>
  1. Ajoutez les vues TextView de la manette de jeu (en d'autres termes, les boutons de jeu), du score et de l'état destinées à l'écran de jeu. Dans le fichier activity_main.xml, remplacez le code par ce qui suit :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <TextView
       android:id="@+id/status"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:text="searching..."
       app:layout_constraintBottom_toTopOf="@+id/myName"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       />

   <TextView
       android:id="@+id/myName"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="You (codeName)"
       android:textAlignment="center"
       android:textAppearance="?android:textAppearanceMedium"
       app:layout_constraintEnd_toStartOf="@+id/opponentName"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/status"
       />

   <TextView
       android:id="@+id/opponentName"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:text="Opponent (codeName)"
       android:textAlignment="center"
       android:textAppearance="?android:textAppearanceMedium"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/myName"
       app:layout_constraintTop_toBottomOf="@+id/status"
       />

   <TextView
       android:id="@+id/score"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_gravity="center"
       android:layout_margin="16dp"
       android:text=":"
       android:textAlignment="center"
       android:textSize="80sp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/myName"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/rock"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/rock"
       android:text="Rock"
       app:layout_constraintEnd_toStartOf="@+id/paper"
       app:layout_constraintHorizontal_chainStyle="spread"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/paper"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/paper"
       android:text="Paper"
       app:layout_constraintEnd_toStartOf="@+id/scissors"
       app:layout_constraintStart_toEndOf="@+id/rock"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/scissors"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableTop="@drawable/scissors"
       android:text="Scissors"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/paper"
       app:layout_constraintTop_toBottomOf="@+id/score"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/disconnect"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_margin="32dp"
       android:text="disconnect"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/paper"
       />

   <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/findOpponent"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_margin="32dp"
       android:text="find opponent"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/paper"
       />

</androidx.constraintlayout.widget.ConstraintLayout>

La mise en page devrait maintenant ressembler à celle de la figure 1 ci-dessus.

Remarque : Dans votre projet, remplacez des valeurs telles que 16dp par des ressources telles que @dimen/activity_vertical_margin.

4. Ajouter Nearby Connections à votre application

Préparer le fichier manifest.xml

Ajoutez les autorisations suivantes au fichier manifeste. Étant donné que ACCESS_FINE_LOCATION est une autorisation dangereuse, votre application inclura du code qui permettra au système de demander en son nom aux utilisateurs d'accorder ou de refuser l'accès. Les autorisations Wi-Fi s'appliquent aux connexions peer-to-peer, et non aux connexions Internet.

<!-- Required for Nearby Connections →

<!--    Because ACCESS_FINE_LOCATION is a dangerous permission, the app will have to-->
<!--    request it at runtime, and the user will be prompted to grant or deny access.-->

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Choisir une stratégie

Pour utiliser l'API Nearby Connections, vous devez sélectionner une stratégie (Strategy) qui déterminera la façon dont votre application se connectera aux autres appareils à proximité. Sélectionnez P2P_CLUSTER, P2P_STAR ou P2P_POINT_TO_POINT.

Dans le cadre de cet atelier de programmation, nous choisirons P2P_STAR, car nous voulons voir un grand nombre de demandes entrantes de la part de joueurs qui veulent nous défier, mais nous ne jouerons qu'avec un seul joueur à la fois.

La stratégie (Strategy) que vous choisissez devra être utilisée à la fois pour annoncer que vous souhaitez communiquer avec d'autres appareils et pour détecter les appareils à proximité dans votre application. La figure ci-dessous illustre le fonctionnement de chaque stratégie (Strategy).

Les appareils peuvent demander N connexions sortantes

Les appareils peuvent recevoir M connexions entrantes

P2P_CLUSTER

N = plusieurs

M = plusieurs

entraîne une réduction de la bande passante utilisée

P2P_STAR

N = 1

M = plusieurs

entraîne une augmentation de la bande passante utilisée

P2P_POINT_TO_POINT

N = 1

M = 1

débit maximal possible

Définir des variables dans MainActivity

  1. Dans l'activité principale (MainActivity.kt), au-dessus de la fonction onCreate(), définissez les variables suivantes en collant cet extrait de code. Ces variables définissent une logique spécifique au jeu et les autorisations d'exécution.
/**
* Enum class for defining the winning rules for Rock-Paper-Scissors. Each player will make a
* choice, then the beats function in this class will be used to determine whom to reward the
* point to.
*/
private enum class GameChoice {
   ROCK, PAPER, SCISSORS;

   fun beats(other: GameChoice): Boolean =
       (this == ROCK && other == SCISSORS)
               || (this == SCISSORS && other == PAPER)
               || (this == PAPER && other == ROCK)
}

/**
* Instead of having each player enter a name, in this sample we will conveniently generate
* random human readable names for players.
*/
internal object CodenameGenerator {
   private val COLORS = arrayOf(
       "Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet", "Purple", "Lavender"
   )
   private val TREATS = arrayOf(
       "Cupcake", "Donut", "Eclair", "Froyo", "Gingerbread", "Honeycomb",
       "Ice Cream Sandwich", "Jellybean", "Kit Kat", "Lollipop", "Marshmallow", "Nougat",
       "Oreo", "Pie"
   )
   private val generator = Random()

   /** Generate a random Android agent codename  */
   fun generate(): String {
       val color = COLORS[generator.nextInt(COLORS.size)]
       val treat = TREATS[generator.nextInt(TREATS.size)]
       return "$color $treat"
   }
}

/**
* Strategy for telling the Nearby Connections API how we want to discover and connect to
* other nearby devices. A star shaped strategy means we want to discover multiple devices but
* only connect to and communicate with one at a time.
*/
private val STRATEGY = Strategy.P2P_STAR

/**
* Our handle to the [Nearby Connections API][ConnectionsClient].
*/
private lateinit var connectionsClient: ConnectionsClient

/**
* The request code for verifying our call to [requestPermissions]. Recall that calling
* [requestPermissions] leads to a callback to [onRequestPermissionsResult]
*/
private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1

/*
The following variables are convenient ways of tracking the data of the opponent that we
choose to play against.
*/
private var opponentName: String? = null
private var opponentEndpointId: String? = null
private var opponentScore = 0
private var opponentChoice: GameChoice? = null

/*
The following variables are for tracking our own data
*/
private var myCodeName: String = CodenameGenerator.generate()
private var myScore = 0
private var myChoice: GameChoice? = null

/**
* This is for wiring and interacting with the UI views.
*/
private lateinit var binding: ActivityMainBinding
  1. Modifiez votre fonction onCreate() pour transmettre l'objet ViewBinding à setContentView(). Le contenu du fichier de mise en page activity_main.xml s'affiche. Initialisez également connectionsClient pour que votre application puisse communiquer avec l'API.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
   connectionsClient = Nearby.getConnectionsClient(this)
}

Vérifier les autorisations requises

En règle générale, les autorisations dangereuses sont déclarées dans le fichier AndroidManifest.xml, mais doivent être demandées au moment de l'exécution. Pour les autres autorisations nécessaires, vous devez toujours les vérifier au moment de l'exécution pour vous assurer que le résultat est conforme à vos attentes. Si l'utilisateur refuse l'une d'entre elles, affichez un toast pour l'informer qu'il ne peut pas continuer sans accorder ces autorisations, car elles sont requises par notre application exemple.

  • Sous la fonction onCreate(), ajoutez l'extrait de code suivant pour vérifier que nous disposons des autorisations :
@CallSuper
override fun onStart() {
   super.onStart()
   if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
       requestPermissions(
           arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
           REQUEST_CODE_REQUIRED_PERMISSIONS
       )
   }
}

@CallSuper
override fun onRequestPermissionsResult(
   requestCode: Int,
   permissions: Array<out String>,
   grantResults: IntArray
) {
   super.onRequestPermissionsResult(requestCode, permissions, grantResults)
   val errMsg = "Cannot start without required permissions"
   if (requestCode == REQUEST_CODE_REQUIRED_PERMISSIONS) {
       grantResults.forEach {
           if (it == PackageManager.PERMISSION_DENIED) {
               Toast.makeText(this, errMsg, Toast.LENGTH_LONG).show()
               finish()
               return
           }
       }
       recreate()
   }
}

À ce stade, nous avons écrit du code pour effectuer les tâches suivantes :

  • Créer un fichier de mise en page
  • Déclarer les autorisations nécessaires dans le fichier manifeste
  • Vérifier les autorisations dangereuses requises au moment de l'exécution

Examiner le code en commençant par la fin

Maintenant que toutes les conditions préliminaires sont remplies, nous pouvons commencer à écrire le code Nearby Connections pour communiquer avec les utilisateurs à proximité. En règle générale, avant de pouvoir commencer à communiquer avec les appareils à proximité, votre application doit rechercher d'autres appareils et autoriser les autres appareils à la détecter.

En d'autres termes, dans le contexte de notre jeu de pierre-papier-ciseaux, vous et vos adversaires devez entrer en contact avant de pouvoir commencer à jouer.

Vous pouvez rendre votre appareil visible grâce à un processus appelé annonce. De même, vous pouvez identifier des adversaires à proximité grâce à un processus appelé détection.

Pour mieux comprendre le processus, il est préférable d'examiner le code en commençant par la fin. Pour ce faire, nous procéderons comme suit :

  1. Nous prétendrons que nous sommes déjà connectés et écrirons le code pour envoyer et recevoir des messages. Dans le cadre de cet atelier, nous écrirons le code du jeu pierre-papier-ciseaux.
  2. Nous créerons le code qui annoncera que nous souhaitons communiquer avec des appareils à proximité.
  3. Nous créerons le code permettant d'identifier les appareils à proximité.

Envoyer et recevoir des données

Vous utiliserez la méthode connectionsClient.sendPayload() pour envoyer des données en tant que Payload et l'objet PayloadCallback pour recevoir les charges utiles. Un Payload peut être n'importe quel élément : vidéo, photo, flux ou tout autre type de données. Il n'est soumis à aucune limite de données.

  1. Dans notre jeu, la charge utile est le choix entre pierre, papier et ciseaux. Lorsqu'un utilisateur clique sur l'un des boutons de la manette, l'application envoie son choix à l'application de l'adversaire en tant que charge utile. Pour enregistrer le choix de l'utilisateur, ajoutez l'extrait de code suivant sous la fonction onRequestPermissionsResult().
/** Sends the user's selection of rock, paper, or scissors to the opponent. */
private fun sendGameChoice(choice: GameChoice) {
   myChoice = choice
   connectionsClient.sendPayload(
       opponentEndpointId!!,
       Payload.fromBytes(choice.name.toByteArray(UTF_8))
   )
   binding.status.text = "You chose ${choice.name}"
   // For fair play, we will disable the game controller so that users don't change their
   // choice in the middle of a game.
   setGameControllerEnabled(false)
}

/**
* Enables/Disables the rock, paper and scissors buttons. Disabling the game controller
* prevents users from changing their minds after making a choice.
*/
private fun setGameControllerEnabled(state: Boolean) {
   binding.apply {
       rock.isEnabled = state
       paper.isEnabled = state
       scissors.isEnabled = state
   }
}
  1. Un appareil reçoit les charges utiles via l'objet PayloadCallback, qui comporte deux méthodes. La méthode onPayloadReceived() indique à votre application lorsqu'elle reçoit un message, et la méthode onPayloadTransferUpdate() suit l'état des messages entrants et sortants.

Pour les besoins de cet atelier, nous lirons le message entrant provenant d'onPayloadReceived() comme l'action de notre adversaire, et nous utiliserons la méthode onPayloadTransferUpdate() pour suivre et confirmer les actions des deux joueurs. Ajoutez cet extrait de code au-dessus de la méthode onCreate().

/** callback for receiving payloads */
private val payloadCallback: PayloadCallback = object : PayloadCallback() {
   override fun onPayloadReceived(endpointId: String, payload: Payload) {
       payload.asBytes()?.let {
           opponentChoice = GameChoice.valueOf(String(it, UTF_8))
       }
   }

   override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
       // Determines the winner and updates game state/UI after both players have chosen.
       // Feel free to refactor and extract this code into a different method
       if (update.status == PayloadTransferUpdate.Status.SUCCESS
           && myChoice != null && opponentChoice != null) {
           val mc = myChoice!!
           val oc = opponentChoice!!
           when {
               mc.beats(oc) -> { // Win!
                   binding.status.text = "${mc.name} beats ${oc.name}"
                   myScore++
               }
               mc == oc -> { // Tie
                   binding.status.text = "You both chose ${mc.name}"
               }
               else -> { // Loss
                   binding.status.text = "${mc.name} loses to ${oc.name}"
                   opponentScore++
               }
           }
           binding.score.text = "$myScore : $opponentScore"
           myChoice = null
           opponentChoice = null
           setGameControllerEnabled(true)
       }
   }
}

Vous annoncez votre présence ou votre intérêt en espérant qu'une personne à proximité vous remarquera et demandera à entrer en contact avec vous. En tant que tel, la méthode startAdvertising() de l'API Nearby Connections nécessite un objet de rappel. Ce rappel, ConnectionLifecycleCallback, vous informe lorsqu'un utilisateur ayant remarqué votre annonce souhaite interagir avec vous. L'objet de rappel comporte trois méthodes :

  • La méthode onConnectionInitiated() vous indique qu'un utilisateur a remarqué votre annonce et qu'il souhaite communiquer avec vous. Par conséquent, vous pouvez choisir d'accepter la communication avec connectionsClient.acceptConnection().
  • Lorsqu'un utilisateur remarque votre annonce, il vous envoie une demande de communication. L'expéditeur et vous-même devez tous les deux accepter cette demande pour entrer en contact. La méthode onConnectionResult() vous permet de savoir si la communication a été établie.
  • La fonction onDisconnected() vous indique que la communication n'est plus active. Cela peut se produire, par exemple, si vous ou l'adversaire décidez de mettre fin à la communication.

Pour annoncer votre présence, procédez comme suit :

  1. Dans notre application, nous accepterons la communication lorsque nous recevrons l'appel onConnectionInitiated(). Puis, dans onConnectionResult(), si la communication a été établie, nous suspendrons l'annonce et la détection, car nous n'avions besoin d'entrer en contact qu'avec un seul adversaire pour jouer. Enfin, nous réinitialiserons le jeu dans onConnectionResult().

Collez l'extrait de code suivant avant la méthode onCreate().

// Callbacks for connections to other devices
private val connectionLifecycleCallback = object : ConnectionLifecycleCallback() {
   override fun onConnectionInitiated(endpointId: String, info: ConnectionInfo) {
       // Accepting a connection means you want to receive messages. Hence, the API expects
       // that you attach a PayloadCall to the acceptance
       connectionsClient.acceptConnection(endpointId, payloadCallback)
       opponentName = "Opponent\n(${info.endpointName})"
   }

   override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
       if (result.status.isSuccess) {
           connectionsClient.stopAdvertising()
           connectionsClient.stopDiscovery()
           opponentEndpointId = endpointId
           binding.opponentName.text = opponentName
           binding.status.text = "Connected"
           setGameControllerEnabled(true) // we can start playing
       }
   }

   override fun onDisconnected(endpointId: String) {
       resetGame()
   }
}
  1. Comme le fait de pouvoir appeler resetGame() à différentes jonctions est si pratique, nous le créerons dans sa propre sous-routine. Ajoutez le code en bas de la classe MainActivity.
/** Wipes all game state and updates the UI accordingly. */
private fun resetGame() {
   // reset data
   opponentEndpointId = null
   opponentName = null
   opponentChoice = null
   opponentScore = 0
   myChoice = null
   myScore = 0
   // reset state of views
   binding.disconnect.visibility = View.GONE
   binding.findOpponent.visibility = View.VISIBLE
   setGameControllerEnabled(false)
   binding.opponentName.text="opponent\n(none yet)"
   binding.status.text ="..."
   binding.score.text = ":"
}
  1. L'extrait de code suivant est l'appel d'annonce réel, dans lequel vous indiquez à l'API Nearby Connections que vous souhaitez passer en mode annonce. Ajoutez-le sous la méthode onCreate().
private fun startAdvertising() {
   val options = AdvertisingOptions.Builder().setStrategy(STRATEGY).build()
   // Note: Advertising may fail. To keep this demo simple, we don't handle failures.
   connectionsClient.startAdvertising(
       myCodeName,
       packageName,
       connectionLifecycleCallback,
       options
   )
}

Détection

La détection vient en complément à l'annonce de votre présence. Les deux appels sont très similaires, sauf qu'ils utilisent des rappels différents. Le rappel pour l'appel startDiscovery() est un objet EndpointDiscoveryCallback. Cet objet comporte deux méthodes de rappel : onEndpointFound() est appelé à chaque fois que l'annonce d'une présence est détectée. onEndpointLost() est appelé à chaque fois que l'annonce d'une présence n'est plus disponible.

  1. Notre application communiquera avec le premier annonceur que nous détecterons. Entre d'autres termes, nous effectuerons une demande de communication dans la méthode onEndpointFound(). Nous ne ferons rien avec la méthode onEndpointLost(). Ajoutez le rappel avant la méthode onCreate().
// Callbacks for finding other devices
private val endpointDiscoveryCallback = object : EndpointDiscoveryCallback() {
   override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
       connectionsClient.requestConnection(myCodeName, endpointId, connectionLifecycleCallback)
   }

   override fun onEndpointLost(endpointId: String) {
   }
}
  1. Ajoutez également l'extrait de code pour indiquer à l'API Nearby Connections que vous souhaitez passer en mode détection. Ajoutez-le en bas de la classe MainActivity.
private fun startDiscovery(){
   val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
   connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
  1. À ce stade, la portion Nearby Connections de notre travail est terminée. Nous pouvons annoncer notre présence, détecter les appareils à proximité et communiquer avec eux. Toutefois, nous ne pouvons pas encore vraiment jouer. Nous devons finir de raccorder les vues de l'interface utilisateur :
  • Lorsque l'utilisateur clique sur le bouton FIND OPPONENT (RECHERCHER UN ADVERSAIRE), l'application doit appeler à la fois startAdvertising() et startDiscovery(). De cette manière, vous pouvez à la fois détecter les appareils à proximité et être détecté.
  • Lorsque l'utilisateur clique sur l'un des boutons de la manette ROCK (PIERRE), PAPER (PAPIER) ou SCISSORS (CISEAUX), l'application doit appeler sendGameChoice() pour transmettre cette information à l'adversaire.
  • Lorsque l'un des utilisateurs clique sur le bouton DISCONNECT (SE DÉCONNECTER), l'application devrait réinitialiser le jeu.

Mettez à jour la méthode onCreate() pour refléter ces interactions.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
   connectionsClient = Nearby.getConnectionsClient(this)

   binding.myName.text = "You\n($myCodeName)"
   binding.findOpponent.setOnClickListener {
       startAdvertising()
       startDiscovery()
       binding.status.text = "Searching for opponents..."
       // "find opponents" is the opposite of "disconnect" so they don't both need to be
       // visible at the same time
       binding.findOpponent.visibility = View.GONE
       binding.disconnect.visibility = View.VISIBLE
   }
   // wire the controller buttons
   binding.apply {
       rock.setOnClickListener { sendGameChoice(GameChoice.ROCK) }
       paper.setOnClickListener { sendGameChoice(GameChoice.PAPER) }
       scissors.setOnClickListener { sendGameChoice(GameChoice.SCISSORS) }
   }
   binding.disconnect.setOnClickListener {
       opponentEndpointId?.let { connectionsClient.disconnectFromEndpoint(it) }
       resetGame()
   }

   resetGame() // we are about to start a new game
}

Effectuer un nettoyage

Vous devez cesser d'utiliser l'API Nearby Connections lorsque vous n'en avez plus besoin. Pour notre exemple de jeu, nous libérerons tous les éléments dans la fonction du cycle de vie de l'activité onStop().

@CallSuper
override fun onStop(){
   connectionsClient.apply {
       stopAdvertising()
       stopDiscovery()
       stopAllEndpoints()
   }
   resetGame()
   super.onStop()
}

5. Exécuter l'application

Exécutez l'application sur deux appareils et profitez du jeu.

e545703b29e0158a.gif

6. Bonnes pratiques en matière de confidentialité

Notre jeu pierre-papier-ciseaux ne partage aucune donnée sensible. Même les noms de code sont générés de manière aléatoire. C'est pourquoi nous acceptons automatiquement la communication dans onConnectionInitiated(String, ConnectionInfo).

L'objet ConnectionInfo contient un jeton unique par communication. Votre application peut y accéder via getAuthenticationDigits(). Vous pouvez présenter les jetons aux deux utilisateurs pour effectuer une validation visuelle. Vous pouvez également chiffrer le jeton brut sur un appareil et l'envoyer en tant que charge utile pour le déchiffrer sur l'autre appareil avant de commencer à partager des données sensibles. Pour en savoir plus sur le chiffrement Android, consultez l'article de blog qui explique comment améliorer la cryptographie de votre application, qu'il s'agisse de l'authentification des messages ou la présence des utilisateurs.

7. Félicitations

Félicitations ! Vous savez maintenant ouvrir la communication entre vos utilisateurs sans connexion Internet via l'API Nearby Connections.

En résumé, pour utiliser l'API Nearby Connections, vous devez ajouter la dépendance play-services-nearby. Vous devez également demander des autorisations dans le fichier AndroidManifest.xml et les vérifier au moment de l'exécution. Vous avez également appris à effectuer les opérations suivantes :

  • Indiquer que vous souhaitez interagir avec les utilisateurs à proximité
  • Identifier les utilisateurs à proximité qui souhaitent entrer en contact avec vous
  • Accepter les communications
  • Envoyer des messages
  • Recevoir des messages
  • Assurer la confidentialité

Et maintenant ?

Consultez notre série d'articles de blog et notre application exemple :