Komunikasi dua arah tanpa internet

1. Sebelum Memulai

Pasti menyenangkan jika Anda dapat menggunakan perangkat seluler untuk berkolaborasi dalam proyek kelompok atau berbagi video, melakukan streaming konten, bermain game multiplayer – bahkan tanpa akses ke internet? Anda bisa melakukannya. Dan dalam codelab ini, Anda akan mempelajari cara melakukannya.

Untuk menyederhanakannya, kita akan membuat game Rock-Paper-Scissors multiplayer yang berfungsi tanpa internet. Codelab ini mengajarkan cara menggunakan Nearby Connections API, bagian dari layanan Google Play, untuk memungkinkan pengguna berkomunikasi satu sama lain berdasarkan kedekatan fisik. Pengguna harus berada dalam jarak sekitar 100 meter dari satu sama lain. Tidak ada batasan jenis atau jumlah data yang dapat dibagikan pengguna - bahkan tanpa koneksi internet. Pengguna dapat melakukan streaming video, mengirim dan menerima pesan suara, mengirim pesan teks, dan lainnya.

Prasyarat

  • Pengetahuan dasar tentang pengembangan Kotlin dan Android
  • Cara membuat dan menjalankan aplikasi di Android Studio
  • Dua perangkat Android atau lebih, untuk menjalankan dan menguji kode
  • menjalankan Android API level 16 atau yang lebih tinggi
  • dengan layanan Google Play yang sudah terinstal
  • Versi terbaru Android Studio.

Yang akan Anda pelajari

  • Cara menambahkan library Nearby Connections layanan Google Play ke aplikasi Anda
  • Cara memberitahukan minat Anda untuk berkomunikasi dengan perangkat di sekitar
  • Cara menemukan perangkat di sekitar yang diminati
  • Cara berkomunikasi dengan perangkat terhubung
  • Praktik terbaik untuk perlindungan data dan privasi

Yang akan Anda build

Codelab ini menampilkan cara membuat satu aplikasi Aktivitas yang memungkinkan pengguna menemukan lawan dan bermain Rock-Paper-Scissors. Aplikasi memiliki elemen UI berikut:

  1. Tombol untuk menemukan lawan
  2. Pengontrol game dengan tiga tombol yang memungkinkan pengguna memilih ROCK, PAPER, atau SCISSORS untuk bermain
  3. TextView untuk menampilkan skor
  4. TextView untuk menampilkan status

625eeebfad3b195a.png

Gambar 1

2. Membuat project Android Studio

  1. Mulai project Android Studio baru.
  2. Pilih Empty Activity.

f2936f15aa940a21.png

  1. Beri nama project Rock Paper Scissors, lalu setel bahasa ke Kotlin.

1ea410364fbdfc31.png

3. Menyiapkan kode

  1. Tambahkan versi terbaru dependensi Nearby ke dalam file build.gradle tingkat aplikasi Anda. Hal ini memungkinkan aplikasi Anda menggunakan Nearby Connections API untuk menunjukkan keinginan untuk terhubung, menemukan perangkat di sekitar, dan berkomunikasi.
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
  1. Tetapkan opsi build viewBinding ke true dalam blok android untuk mengaktifkan View Binding, sehingga Anda tidak perlu menggunakan findViewById untuk berinteraksi dengan Views.
android {
   ...
   buildFeatures {
       viewBinding true
   }
}
  1. Klik Sync Now atau tombol palu hijau sehingga Android Studio mempertimbangkan perubahan Gradle ini.

57995716c771d511.png

  1. Kami menggunakan vektor drawable untuk gambar batu, kertas, dan gunting. Tambahkan tiga file XML berikut ke direktori 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. Tambahkan pengontrol game (atau tombol bermain), skor, dan status TextView untuk layar game. Di file activity_main.xml, ganti kode dengan kode berikut:
<?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>

Tata letak Anda kini akan terlihat seperti di Gambar 1 di atas.

Catatan: Di project Anda sendiri, ubah nilai seperti 16dp ke resource seperti @dimen/activity_vertical_margin.

4. Menambahkan Koneksi Di Sekitar ke aplikasi

Menyiapkan file manifest.xml

Tambahkan izin berikut ke file manifes. Karena ACCESS_FINE_LOCATION adalah izin berbahaya, aplikasi Anda akan menyertakan kode yang akan memicu sistem untuk meminta pengguna atas nama aplikasi Anda untuk memberikan atau menolak akses. Izin Wi-Fi berlaku untuk koneksi peer-to-peer, bukan koneksi 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"/>

Memilih Strategi

Nearby Connections API mengharuskan Anda memilih Strategy yang menentukan cara aplikasi terhubung dengan perangkat di sekitar yang lain. Pilih P2P_CLUSTER, P2P_STAR, atau P2P_POINT_TO_POINT.

Untuk tujuan ini, kita akan memilih P2P_STAR karena kita ingin melihat banyak permintaan masuk dari pemain yang ingin menantang kita, tetapi hanya bermain satu lawan satu.

Strategy yang Anda pilih harus digunakan untuk iklan dan penemuan dalam aplikasi. Gambar di bawah menunjukkan cara kerja setiap Strategy.

Perangkat dapat meminta N koneksi keluar

Perangkat dapat menerima M koneksi masuk

P2P_CLUSTER

N = banyak

M = banyak

menghasilkan koneksi bandwidth yang lebih rendah

P2P_STAR

N = 1

M = banyak

menghasilkan koneksi dengan bandwidth yang lebih tinggi

P2P_POINT_TO_POINT

N = 1

M = 1

throughput setinggi mungkin

Menentukan variabel dalam MainActivity

  1. Di dalam aktivitas utama (MainActivity.kt), di atas fungsi onCreate(), tentukan variabel berikut dengan menempelkan cuplikan kode ini. Variabel ini menentukan logika khusus game dan izin runtime.
/**
* 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. Ubah fungsi onCreate() Anda untuk meneruskan objek ViewBinding ke setContentView(). Ini akan menampilkan isi file tata letak activity_main.xml. Selain itu, inisialisasi connectionsClient agar aplikasi Anda dapat berkomunikasi dengan API.
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
   connectionsClient = Nearby.getConnectionsClient(this)
}

Verifikasi izin yang diperlukan

Sebagai aturan, izin berbahaya dinyatakan dalam file AndroidManifest.xml, tetapi harus diminta pada runtime. Untuk izin lain yang diperlukan, Anda tetap harus memverifikasinya saat runtime untuk memastikan hasilnya sesuai harapan. Dan jika pengguna menolak salah satunya, tampilkan toast yang memberi tahu bahwa mereka tidak dapat melanjutkan tanpa memberikan izin karena aplikasi contoh tidak dapat digunakan tanpa izin tersebut.

  • Di bawah fungsi onCreate(), tambahkan cuplikan kode berikut untuk memverifikasi bahwa kita memiliki izin:
@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()
   }
}

Pada tahap ini, kita telah menulis kode untuk menyelesaikan tugas-tugas berikut:

  • Membuat file tata letak
  • Mendeklarasikan izin yang diperlukan dalam manifes
  • Memverifikasi izin berbahaya yang diperlukan saat runtime

Panduan terbalik

Setelah persiapan selesai, kita siap memulai menulis kode Nearby Connections untuk terhubung dengan pengguna di sekitar dan berkomunikasi. Biasanya, sebelum Anda dapat benar-benar mulai berkomunikasi dengan perangkat di sekitar, aplikasi Anda harus mengizinkan perangkat lain untuk menemukannya dan aplikasi tersebut harus memindai perangkat lain.

Dengan kata lain, dalam konteks game Rock-Paper-Scissors, Anda dan lawan harus menemukan satu sama lain sebelum dapat mulai bermain game.

Anda dapat membuat perangkat dapat ditemukan melalui proses yang disebut pemberitahuan. Demikian pula, Anda dapat menemukan lawan di sekitar melalui proses yang disebut penemuan.

Agar dapat memahami proses ini, sebaiknya kode dipelajari secara terbalik. Untuk itu, kita akan melakukan hal berikut:

  1. Kita akan berpura-pura telah terhubung dan menulis kode untuk mengirim dan menerima pesan. Untuk tujuan kita sekarang, ini berarti menulis kode untuk memainkan game Rock-Paper-Scissors.
  2. Kita akan menulis kode untuk memberi tahu minat kita untuk terhubung dengan perangkat di sekitar.
  3. Kita akan menulis kode untuk menemukan perangkat di sekitar.

Mengirim dan menerima data

Anda dapat menggunakan metode connectionsClient.sendPayload() untuk mengirim data sebagai Payload, dan objek PayloadCallback untuk menerima payload. Payload dapat berupa apa saja: video, foto, streaming, atau jenis data lainnya. Dan tidak ada batas data.

  1. Dalam game kita, payload adalah pilihan batu, kertas, atau gunting. Saat pengguna mengklik salah satu tombol pengontrol, aplikasi akan mengirimkan pilihannya ke aplikasi lawan sebagai payload. Untuk merekam perpindahan pengguna, tambahkan cuplikan kode berikut di bawah fungsi 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. Perangkat akan menerima payload melalui objek PayloadCallback, yang memiliki dua metode. Metode onPayloadReceived() memberi tahu aplikasi Anda saat menerima pesan, dan metode onPayloadTransferUpdate() melacak status pesan masuk dan keluar.

Untuk tujuan ini, kita akan membaca pesan masuk dari onPayloadReceived() saat lawan kita bergerak, dan menggunakan metode onPayloadTransferUpdate() untuk melacak dan mengonfirmasi ketika kedua pemain melakukan gerakan. Tambahkan cuplikan kode ini di atas metode 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)
       }
   }
}

Anda memberitahukan kehadiran atau minat Anda dengan harapan bahwa seseorang di sekitar akan melihat Anda dan meminta untuk terhubung dengan Anda. Oleh karena itu, metode startAdvertising() dari Nearby Connections API memerlukan objek callback. Callback tersebut, ConnectionLifecycleCallback, akan memberi tahu Anda saat seseorang yang melihat pemberitahuan Anda ingin terhubung. Objek callback memiliki tiga metode:

  • Metode onConnectionInitiated() menunjukkan bahwa seseorang telah melihat pemberitahuan Anda dan ingin terhubung. Hasilnya, Anda dapat memilih untuk menerima koneksi dengan connectionsClient.acceptConnection().
  • Saat seseorang melihat pemberitahuan Anda, orang tersebut mengirimkan permintaan koneksi kepada Anda. Baik Anda maupun pengirim harus menerima permintaan koneksi agar benar-benar terhubung. Metode onConnectionResult() memberi tahu Anda apakah koneksi sudah terbentuk.
  • Fungsi onDisconnected() memberi tahu Anda bahwa koneksi tidak lagi aktif. Hal ini dapat terjadi, misalnya, jika Anda atau lawan memutuskan untuk mengakhiri koneksi.

Untuk membuat pemberitahuan:

  1. Untuk aplikasi, kita akan menerima koneksi saat mendapatkan callback onConnectionInitiated(). Kemudian, di dalam onConnectionResult(), jika koneksi terbentuk, kita akan menghentikan pemberitahuan dan penemuan karena kita hanya perlu terhubung dengan satu lawan untuk bermain game. Dan terakhir, dalam onConnectionResult(), kita akan mereset game.

Tempel cuplikan kode berikut sebelum metode onCreate() Anda.

// 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. Karena resetGame() mudah dipanggil kapan saja, kita akan membuatnya menjadi subrutinitasnya sendiri. Tambahkan kode di bagian bawah class 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. Cuplikan berikut adalah panggilan pemberitahuan yang sebenarnya, ketika Anda memberi tahu Nearby Connections API bahwa Anda ingin memasuki mode pemberitahuan. Tambahkan di bawah metode onCreate() Anda.
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
   )
}

Penemuan

Pelengkap pemberitahuan adalah penemuan. Kedua panggilan tersebut sangat mirip, namun menggunakan callback yang berbeda. Callback untuk panggilan startDiscovery() adalah objek EndpointDiscoveryCallback. Objek ini memiliki dua metode callback: onEndpointFound() dipanggil setiap kali pemberitahuan terdeteksi; onEndpointLost() dipanggil setiap kali pemberitahuan tidak lagi tersedia.

  1. Aplikasi kita akan terhubung dengan pengiklan pertama yang terdeteksi. Ini berarti bahwa kita akan membuat permintaan koneksi dalam metode onEndpointFound(), dan tidak melakukan apa pun dengan metode onEndpointLost(). Tambahkan callback sebelum metode 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. Tambahkan juga cuplikan agar benar-benar memberi tahu Nearby Connections API bahwa Anda ingin masuk ke mode penemuan. Tambahkan di bagian bawah kelas MainActivity Anda.
private fun startDiscovery(){
   val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
   connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
  1. Pada tahap ini, bagian Nearby Connections dari pekerjaan kita telah selesai! Kita dapat memberi tahu, menemukan, dan berkomunikasi dengan perangkat di sekitar. Namun, kita belum bisa memainkan game tersebut. Kita harus menyelesaikan pemasangan tampilan UI:
  • Saat pengguna mengklik tombol FIND OPPONENT, aplikasi akan memanggil startAdvertising() dan startDiscovery(). Dengan cara ini, Anda sedang menemukan dan ditemukan.
  • Saat pengguna mengklik salah satu tombol pengontrol ROCK, PAPER, atau SCISSORS, aplikasi perlu memanggil sendGameChoice() untuk mengirimkan data tersebut ke lawan.
  • Saat pengguna mengklik tombol DISCONNECT, aplikasi akan mereset game.

Perbarui metode onCreate() Anda untuk mencerminkan interaksi ini.

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
}

Membersihkan

Anda sebaiknya berhenti menggunakan Nearby API jika tidak diperlukan lagi. Untuk game contoh, kita merilis semua aset di dalam fungsi siklus proses aktivitas onStop().

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

5. Menjalankan aplikasi

Jalankan aplikasi di dua perangkat dan nikmati gamenya!

e545703b29e0158a.gif

6. Praktik terbaik untuk privasi

Game Rock-Paper-Scissors kita tidak membagikan data sensitif apa pun. Bahkan nama kode dihasilkan secara acak. Itulah sebabnya kita secara otomatis menerima koneksi di dalam onConnectionInitiated(String, ConnectionInfo).

Objek ConnectionInfo berisi token unik per koneksi, yang dapat diakses aplikasi Anda melalui getAuthenticationDigits(). Anda dapat menampilkan token kepada kedua pengguna untuk verifikasi visual. Sebagai alternatif, Anda dapat mengenkripsi token mentah di satu perangkat dan mengirimnya sebagai payload yang akan didekripsi di perangkat lain sebelum mulai berbagi data sensitif. Untuk informasi selengkapnya tentang enkripsi Android, lihat postingan blog yang berjudul "Meningkatkan kriptografi aplikasi Anda, dari autentikasi pesan hingga kehadiran pengguna".

7. Selamat

Selamat! Kini Anda mengetahui cara menghubungkan pengguna tanpa koneksi internet melalui Nearby Connections API.

Singkatnya, untuk menggunakan Nearby Connections API, Anda perlu menambahkan dependensi untuk play-services-nearby. Anda juga perlu meminta izin dalam file AndroidManifest.xml dan memeriksa izin tersebut saat runtime. Anda juga telah mempelajari cara melakukan hal-hal berikut:

  • Memberi tahu minat Anda untuk terhubung dengan pengguna di sekitar
  • Menemukan pengguna di sekitar yang ingin terhubung
  • Menerima koneksi
  • Mengirim pesan
  • Menerima pesan
  • Menjaga privasi pengguna

Apa Selanjutnya?

Lihat rangkaian blog dan contoh kami