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 :
- Un bouton permettant de rechercher des adversaires
- Une manette de jeu avec trois boutons permettant aux utilisateurs de choisir ROCK (PIERRE), PAPER (PAPIER) ou SCISSORS (CISEAUX)
- Des vues
TextView
pour l'affichage des scores - Une vue
TextView
pour l'affichage de l'état
Figure 1
2. Créer un projet Android Studio
- Lancez un nouveau projet Android Studio.
- Sélectionnez Empty Activity (Activité vide).
- Nommez le projet Rock Paper Scissors (Pierre, papier, ciseaux) et définissez le langage sur Kotlin.
3. Configurer le code
- 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'
- Définissez l'option de compilation viewBinding sur
true
dans le blocandroid
pour activer l'option View Binding (Liaison de vue). De cette manière, vous n'aurez pas à utiliserfindViewById
pour interagir avec les vues.
android {
...
buildFeatures {
viewBinding true
}
}
- Cliquez sur Sync Now (Synchroniser) ou sur le bouton en forme de marteau vert pour qu'Android Studio tienne compte de ces modifications Gradle.
- 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>
- 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 fichieractivity_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 | |||
| N = plusieurs | M = plusieurs | entraîne une réduction de la bande passante utilisée | |
| N = 1 | M = plusieurs | entraîne une augmentation de la bande passante utilisée | |
| N = 1 | M = 1 | débit maximal possible |
Définir des variables dans MainActivity
- Dans l'activité principale (
MainActivity.kt
), au-dessus de la fonctiononCreate()
, 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
- Modifiez votre fonction
onCreate()
pour transmettre l'objetViewBinding
àsetContentView()
. Le contenu du fichier de mise en pageactivity_main.xml
s'affiche. Initialisez égalementconnectionsClient
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 :
- 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.
- Nous créerons le code qui annoncera que nous souhaitons communiquer avec des appareils à proximité.
- 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.
- 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
}
}
- Un appareil reçoit les charges utiles via l'objet
PayloadCallback
, qui comporte deux méthodes. La méthodeonPayloadReceived()
indique à votre application lorsqu'elle reçoit un message, et la méthodeonPayloadTransferUpdate()
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)
}
}
}
Annonce de votre présence
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 avecconnectionsClient.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 :
- Dans notre application, nous accepterons la communication lorsque nous recevrons l'appel
onConnectionInitiated()
. Puis, dansonConnectionResult()
, 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 dansonConnectionResult()
.
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()
}
}
- 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 classeMainActivity
.
/** 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 = ":"
}
- 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.
- 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éthodeonEndpointLost()
. Ajoutez le rappel avant la méthodeonCreate()
.
// 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) {
}
}
- 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)
}
- À 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()
etstartDiscovery()
. 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.
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 :