1. Trước khi bắt đầu
Chẳng phải sẽ thật tốt nếu bạn có thể sử dụng thiết bị di động để cộng tác trong các dự án nhóm hoặc chia sẻ video, phát trực tuyến nội dung, chơi trò chơi nhiều người chơi – ngay cả khi không có kết nối Internet? Bạn thực sự có thể làm được. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách làm việc đó.
Để đơn giản hoá mọi thứ, chúng ta sẽ xây dựng trò chơi Rock-Paper-Scissors (Oẳn tù tì) nhiều người chơi dùng được mà không cần Internet. Lớp học lập trình này hướng dẫn bạn cách sử dụng Nearby Connections API (API Kết nối lân cận) – một phần trong Dịch vụ Google Play cho phép người dùng giao tiếp với nhau dựa trên độ gần thực tế. Người dùng phải cách nhau trong vòng 100 mét. Không có giới hạn về kiểu dữ liệu hoặc lượng dữ liệu mà người dùng có thể chia sẻ – ngay cả khi không có kết nối Internet. Người dùng có thể phát trực tuyến video, gửi và nhận tin nhắn thoại, gửi tin nhắn văn bản, v.v.
Điều kiện tiên quyết
- Có kiến thức cơ bản về Kotlin và phát triển Android
- Biết cách tạo và chạy ứng dụng trên Android Studio
- Hai hoặc nhiều thiết bị Android, để chạy và kiểm thử mã
- chạy Android API cấp độ 16 trở lên
- có cài đặt Dịch vụ Google Play
- Phiên bản mới nhất của Android Studio.
Kiến thức bạn sẽ học được
- Cách thêm thư viện Nearby Connections (Kết nối lân cận) trong Dịch vụ Google Play vào ứng dụng
- Cách thể hiện ý muốn được giao tiếp với các thiết bị ở gần
- Cách khám phá các thiết bị lân cận có quan tâm
- Cách giao tiếp với các thiết bị đã kết nối
- Các phương pháp hay nhất để bảo vệ dữ liệu và quyền riêng tư
Sản phẩm bạn sẽ tạo ra
Lớp học lập trình này hướng dẫn bạn cách tạo một ứng dụng có duy nhất một Hoạt động (Activity) cho phép người dùng tìm đối thủ và chơi trò chơi Rock-Paper-Scissors (Oẳn tù tì). Ứng dụng này có các thành phần giao diện người dùng sau đây:
- Một nút (button) để tìm đối thủ
- Tay điều khiển trò chơi có ba nút cho phép người dùng chọn (ROCK) ĐÁ, PAPER (GIẤY) hoặc SCISSORS (KÉO) để chơi
- Các
TextView
cho thấy điểm số - Một
TextView
cho thấy trạng thái
Hình 1
2. Tạo một dự án Android Studio
- Bắt đầu một dự án Android Studio mới.
- Chọn Empty Activity (Hoạt động trống).
- Đặt tên Rock Paper Scissors cho dự án và đặt ngôn ngữ thành Kotlin.
3. Thiết lập mã
- Thêm phiên bản mới nhất của phần phụ thuộc Nearby (Chia sẻ lân cận) vào tệp
build.gradle
cấp độ ứng dụng. Việc này cho phép ứng dụng của bạn dùng Nearby Connections API để thể hiện rằng bạn có ý muốn được kết nối, khám phá thiết bị ở gần và giao tiếp.
implementation 'com.google.android.gms:play-services-nearby:LATEST_VERSION'
- Đặt tuỳ chọn xây dựng viewBinding thành
true
trong khốiandroid
để bật tính năng View Binding (Liên kết khung hiển thị), nhờ đó bạn không phải sử dụngfindViewById
tương tác với Views (Khung hiển thị).
android {
...
buildFeatures {
viewBinding true
}
}
- Nhấp vào Sync Now (Đồng bộ hoá ngay) hoặc nút hình búa màu xanh lục để Android Studio xem xét những thay đổi Gradle này.
- Chúng ta sẽ sử dụng các vectơ có thể vẽ dành cho hình ảnh đá, giấy và kéo. Thêm ba tệp XML sau vào thư mục
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>
- Thêm tay điều khiển trò chơi (nói cách khác là các nút chơi), điểm và trạng thái của
TextView
cho màn hình trò chơi. Trong tệpactivity_main.xml
, hãy thay thế mã bằng đoạn sau:
<?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>
Bố cục của bạn bây giờ sẽ trông giống như trong Hình 1 ở trên.
Lưu ý: Trong dự án riêng của bạn, hãy thay đổi các giá trị như 16dp
thành các tài nguyên như @dimen/activity_vertical_margin
.
4. Thêm tính năng Nearby Connections (Kết nối lân cận) vào ứng dụng
Chuẩn bị tệp manifest.xml
Thêm các quyền sau đây vào tệp kê khai. Do ACCESS_FINE_LOCATION
là một quyền nguy hiểm nên ứng dụng của bạn sẽ chứa mã kích hoạt hệ thống để thay ứng dụng nhắc người dùng cấp hoặc từ chối quyền truy cập. Quyền truy cập Wi-Fi chỉ áp dụng cho các kết nối ngang hàng, không phải kết nối 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"/>
Chọn một chiến lược
Nearby Connections API yêu cầu bạn chọn Strategy
nhằm xác định cách ứng dụng kết nối với các thiết bị ở gần khác. Chọn P2P_CLUSTER
, P2P_STAR
hoặc P2P_POINT_TO_POINT
.
Đối với mục đích của chúng ta, P2P_STAR
sẽ được chọn vì chúng ta muốn thấy nhiều yêu cầu đến từ những người chơi muốn thử thách chúng ta, nhưng chỉ chơi với một người tại một thời điểm.
Bạn phải sử dụng Strategy
mà bạn chọn cho cả việc quảng cáo (advertising) lẫn khám phá (discovery) trong ứng dụng. Hình bên dưới cho thấy cách hoạt động của từng Strategy
.
Thiết bị có thể yêu cầu N kết nối đi | Thiết bị có thể nhận M kết nối đến | |||
| N = nhiều | M = nhiều | dẫn đến băng thông của các kết nối thấp hơn | |
| N = 1 | M = nhiều | dẫn đến băng thông của các kết nối cao hơn | |
| N = 1 | M = 1 | thông lượng cao nhất có thể |
Khai báo biến trong MainActivity
- Bên trong hoạt động chính (
MainActivity.kt
), phía trên hàmonCreate()
, hãy khai báo các biến sau bằng cách dán đoạn mã này. Các biến này xác định logic cụ thể của trò chơi cũng như các quyền trong thời gian chạy.
/**
* 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
- Thay đổi hàm
onCreate()
của bạn để chuyển đối tượngViewBinding
thànhsetContentView()
. Thao tác này sẽ hiện nội dung của tệp bố cụcactivity_main.xml
. Ngoài ra, hãy khởi độngconnectionsClient
để ứng dụng có thể giao tiếp với API.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
connectionsClient = Nearby.getConnectionsClient(this)
}
Xác minh quyền cần thiết
Theo quy tắc, các quyền nguy hiểm được khai báo trong tệp AndroidManifest.xml
nhưng phải được yêu cầu trong thời gian chạy. Đối với các quyền cần thiết khác, bạn vẫn nên xác minh các quyền đó trong thời gian chạy để đảm bảo kết quả như mong đợi. Và nếu người dùng từ chối quyền nào trong số đó, hãy hiện một thông báo ngắn để người dùng biết rằng họ không thể tiếp tục nếu không cấp các quyền này vì ứng dụng mẫu sẽ không chạy được nếu thiếu các quyền đó.
- Bên dưới hàm
onCreate()
, hãy thêm đoạn mã sau đây để xác minh rằng chúng ta có các quyền cần thiết:
@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()
}
}
Tới đây, chúng ta đã viết mã để hoàn thành các nhiệm vụ sau:
- Đã tạo tệp bố cục
- Đã khai báo quyền cần thiết trong tệp kê khai
- Đã xác minh quyền nguy hiểm cần có trong thời gian chạy
Đảo ngược hướng dẫn từng bước
Hiện tại, chúng ta đã xử lý xong các bước sơ bộ. Chúng ta đã sẵn sàng bắt đầu viết mã Nearby Connections (Kết nối lân cận) để kết nối với người dùng ở gần và giao tiếp. Thông thường, để có thể thực sự bắt đầu giao tiếp với thiết bị ở gần, ứng dụng của bạn phải cho phép thiết bị khác tìm thấy, đồng thời cũng phải quét tìm các thiết bị khác.
Nói cách khác, trong ngữ cảnh của trò chơi Rock-Paper-Scissors, bạn và đối thủ phải tìm thấy nhau để có thể bắt đầu trò chơi.
Bạn có thể giúp các thiết bị lân cận khám phá ra thiết bị của mình thông qua một quá trình có tên là quảng cáo (advertising). Tương tự như vậy, bạn có thể tìm ra các đối thủ lân cận thông qua một quá trình gọi là khám phá (discovery).
Để hiểu được quy trình, tốt nhất bạn nên xử lý mã theo thứ tự đảo ngược. Để đạt được mục tiêu đó, chúng ta sẽ tiếp tục như sau:
- Chúng ta sẽ giả sử rằng mình đã kết nối và viết mã để gửi và nhận tin nhắn. Theo mục đích hiện tại của chúng ta, việc viết mã này là để thực sự chơi trò chơi Rock-Paper-Scissors.
- Chúng ta sẽ viết mã để quảng cáo việc chúng ta muốn kết nối với các thiết bị lân cận.
- Chúng ta sẽ viết mã để khám phá các thiết bị lân cận.
Gửi và nhận dữ liệu
Bạn sử dụng phương thức connectionsClient.sendPayload()
để gửi dữ liệu dưới dạng Payload
và sử dụng đối tượng PayloadCallback
để nhận các gói dữ liệu (payload). Payload
có thể là nội dung bất kỳ: video, ảnh, luồng phát hoặc loại dữ liệu khác. Không có giới hạn về dữ liệu.
- Trong trò chơi của chúng ta, gói dữ liệu này là một lựa chọn (đá, giấy hoặc kéo). Khi người dùng nhấp vào một trong các nút điều khiển, ứng dụng của họ sẽ gửi lựa chọn đó đến ứng dụng của đối thủ dưới dạng một gói dữ liệu. Để ghi lại lượt chơi của người dùng, hãy thêm đoạn mã sau đây vào bên dưới hàm
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
}
}
- Một thiết bị nhận các gói dữ liệu qua đối tượng
PayloadCallback
có hai phương thức. Phương thứconPayloadReceived()
sẽ thông báo cho ứng dụng của bạn khi nhận được tin nhắn, còn phương thứconPayloadTransferUpdate()
sẽ theo dõi trạng thái của cả tin nhắn đến và đi.
Để phục vụ mục đích của mình, chúng ta sẽ đọc tin nhắn đến từ onPayloadReceived()
khi đối thủ thực hiện lượt chơi, đồng thời sử dụng phương thức onPayloadTransferUpdate()
để theo dõi và xác nhận thời điểm cả hai người đã thực hiện xong lượt chơi của họ. Thêm đoạn mã này vào phía trên phương thức 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)
}
}
}
Quảng cáo
Bạn quảng cáo sự hiện diện hoặc mối quan tâm của mình với hy vọng ai đó ở gần sẽ chú ý đến và đề nghị kết nối với bạn. Do đó, phương thức startAdvertising()
của Nearby Connections API yêu cầu một đối tượng gọi lại (callback object). Lệnh gọi lại ConnectionLifecycleCallback
sẽ thông báo cho bạn khi có người nào đó chú ý đến quảng cáo của bạn và muốn kết nối. Đối tượng gọi lại có 3 phương thức:
- Phương thức
onConnectionInitiated()
cho bạn biết rằng có ai đó chú ý đến quảng cáo của bạn và muốn kết nối. Kết quả là bạn có thể chọn chấp nhận kết nối vớiconnectionsClient.acceptConnection()
. - Khi có người chú ý đến quảng cáo của bạn, họ sẽ gửi cho bạn một yêu cầu kết nối. Cả bạn và người gửi đều phải chấp nhận yêu cầu kết nối để thực sự kết nối. Phương thức
onConnectionResult()
cho bạn biết liệu kết nối đã được thiết lập hay chưa. - Hàm
onDisconnected()
cho bạn biết rằng kết nối không còn hoạt động. Điều này có thể xảy ra, chẳng hạn như khi bạn hoặc đối thủ quyết định chấm dứt kết nối.
Cách quảng cáo:
- Đối với ứng dụng của chúng ta, chúng ta sẽ chấp nhận kết nối khi nhận lại lệnh gọi
onConnectionInitiated()
. Sau đó, bên trongonConnectionResult()
, nếu kết nối đã được thiết lập, chúng ta sẽ ngừng quảng cáo và khám phá vì chúng ta chỉ cần kết nối với một đối thủ để chơi trò chơi. Và cuối cùng, trongonConnectionResult()
, chúng ta sẽ đặt lại trò chơi này.
Hãy dán đoạn mã sau trước phương thức 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()
}
}
- Vì rất thuận tiện khi gọi
resetGame()
tại nhiều điểm nối (juncture) nên chúng ta sẽ biến phương thức này thành chương trình con (subroutine) của chính nó. Hãy thêm mã này ở phần cuối của lớpMainActivity
.
/** 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 = ":"
}
- Đoạn mã sau đây là lệnh gọi quảng cáo thực tế, trong đó bạn cho Nearby Connections API biết rằng bạn muốn chuyển sang chế độ quảng cáo (advertising mode). Hãy thêm mã này bên dưới phương thức
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
)
}
Khám phá
Phần bổ sung cho quảng cáo là khám phá. Hai lệnh gọi này là rất giống nhau, ngoại trừ việc sử dụng các lệnh gọi lại riêng biệt. Lệnh gọi lại cho lệnh gọi startDiscovery()
là một đối tượng EndpointDiscoveryCallback
. Đối tượng này có hai phương thức gọi lại: onEndpointFound()
được gọi khi phát hiện thấy một quảng cáo, còn onEndpointLost()
được gọi khi một quảng cáo không còn nữa.
- Ứng dụng của chúng ta sẽ kết nối với người quảng cáo đầu tiên chúng ta phát hiện thấy. Tức là chúng ta sẽ tạo một yêu cầu kết nối bên trong phương thức
onEndpointFound()
và không xử lý gì trong phương thứconEndpointLost()
. Hãy thêm lệnh gọi lại vào trước phương thứconCreate()
.
// 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) {
}
}
- Ngoài ra, hãy thêm đoạn mã này để thực sự thông báo cho Nearby Connections API biết rằng bạn muốn chuyển sang chế độ khám phá (discovery mode). Hãy thêm mã này ở phần cuối của lớp
MainActivity
.
private fun startDiscovery(){
val options = DiscoveryOptions.Builder().setStrategy(STRATEGY).build()
connectionsClient.startDiscovery(packageName,endpointDiscoveryCallback,options)
}
- Đến thời điểm này, phần Nearby Connections (Kết nối lân cận) trong bài tập của chúng ta đã xong! Chúng ta có thể quảng cáo, khám phá và giao tiếp với các thiết bị lân cận. Nhưng chúng ta chưa thể chơi trò chơi này. Chúng ta phải hoàn tất việc kết nối các thành phần hiển thị giao diện người dùng:
- Khi người dùng nhấp vào nút FIND OPPONENT (TÌM ĐỐI THỦ), ứng dụng sẽ gọi cả
startAdvertising()
vàstartDiscovery()
. Bằng cách này, bạn vừa khám phá vừa được khám phá. - Khi người dùng nhấp vào một trong các nút điều khiển của ROCK (ĐÁ), PAPER (GIẤY), hoặc SCISSORS (KÉO), ứng dụng cần gọi
sendGameChoice()
để truyền dữ liệu đó cho đối thủ. - Khi một trong hai người dùng nhấp vào nút DISCONNECT (NGỪNG KẾT NỐI), ứng dụng sẽ đặt lại trò chơi.
Cập nhật phương thức onCreate()
để phản ánh các lượt tương tác này.
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
}
Dọn dẹp
Bạn nên ngừng sử dụng Nearby API khi không còn cần thiết nữa. Đối với trò chơi mẫu của mình, chúng ta huỷ bỏ tất cả tài sản bên trong hàm vòng đời hoạt động onStop()
.
@CallSuper
override fun onStop(){
connectionsClient.apply {
stopAdvertising()
stopDiscovery()
stopAllEndpoints()
}
resetGame()
super.onStop()
}
5. Chạy ứng dụng
Chạy ứng dụng trên hai thiết bị và tận hưởng trò chơi!
6. Phương pháp hay nhất về quyền riêng tư
Trò chơi Rock-Paper-Scissors của chúng ta không chia sẻ dữ liệu nhạy cảm nào. Ngay cả tên mã cũng được tạo ngẫu nhiên. Đó là lý do chúng ta tự động chấp nhận kết nối bên trong onConnectionInitiated(String, ConnectionInfo)
.
Đối tượng ConnectionInfo
chứa một mã thông báo duy nhất cho mỗi kết nối mà ứng dụng của bạn có thể truy cập được thông qua getAuthenticationDigits()
. Bạn có thể hiện mã thông báo cho cả hai người dùng để xác minh bằng hình ảnh. Ngoài ra, bạn có thể mã hoá mã thông báo thô (raw token) trên một thiết bị rồi gửi dưới dạng gói dữ liệu để giải mã trên thiết bị khác trước khi bạn bắt đầu chia sẻ dữ liệu nhạy cảm. Để biết thêm về quá trình mã hoá Android, hãy xem bài đăng trên blog có tên " Improve your app's cryptography, from message authentication to user presence" (Cải thiện khía cạnh mã hoá của ứng dụng, từ xác thực thông báo đến sự hiện diện của người dùng).
7. Xin chúc mừng
Xin chúc mừng! Giờ đây, bạn đã biết cách dùng Nearby Connections API để kết nối người dùng với nhau mà không cần kết nối Internet.
Tóm lại, để sử dụng Nearby Connections API, bạn cần thêm phần phụ thuộc cho play-services-nearby
. Bạn cũng cần yêu cầu các quyền trong tệp AndroidManifest.xml
và kiểm tra các quyền đó trong thời gian chạy. Bạn cũng đã biết cách thực hiện các việc sau:
- Quảng cáo mối quan tâm kết nối với người dùng lân cận
- Khám phá những người dùng lân cận muốn kết nối
- Chấp nhận kết nối
- Gửi tin nhắn
- Nhận tin nhắn
- Bảo vệ quyền riêng tư của người dùng
Tiếp theo là gì?
Hãy xem loạt blog và mẫu của chúng tôi