Hiện địa điểm lân cận trong môi trường thực tế tăng cường trên Android (Kotlin)

1. Trước khi bắt đầu

Trừu tượng

Lớp học lập trình này hướng dẫn bạn cách sử dụng dữ liệu từ Nền tảng Google Maps để hiển thị các địa điểm lân cận ở chế độ thực tế tăng cường (AR) trên Android.

2344909dd9a52c60.png

Điều kiện tiên quyết

  • Có kiến thức cơ bản về hoạt động phát triển Android bằng Android Studio
  • Quen thuộc với Kotlin

Kiến thức bạn sẽ học được

  • Yêu cầu người dùng cấp quyền truy cập vào camera và thông tin vị trí của thiết bị.
  • Tích hợp với Places API để tìm nạp các địa điểm lân cận xung quanh vị trí của thiết bị.
  • Tích hợp với ARCore để tìm các bề mặt phẳng ngang, nhờ đó các đối tượng ảo có thể được cố định và đặt trong không gian 3D bằng Sceneform.
  • Thu thập thông tin về vị trí của thiết bị trong không gian bằng SensorManager và sử dụng Thư viện tiện ích Maps SDK cho Android để đặt các đối tượng ảo ở tiêu đề chính xác.

Bạn cần có

2. Bắt đầu thiết lập

Android Studio

Lớp học lập trình này sử dụng Android 10.0 (API cấp 29) và yêu cầu bạn cài đặt Dịch vụ Google Play trong Android Studio. Để cài đặt cả hai phần phụ thuộc này, hãy hoàn tất các bước sau:

  1. Chuyển đến Trình quản lý SDK. Bạn có thể truy cập vào trình quản lý này bằng cách nhấp vào Tools (Công cụ) > SDK Manager (Trình quản lý SDK).

6c44a9cb9cf6c236.png

  1. Kiểm tra xem bạn đã cài đặt Android 10.0 hay chưa. Nếu chưa, hãy cài đặt bằng cách chọn hộp đánh dấu bên cạnh Android 10.0 (Q), sau đó nhấp vào OK rồi nhấp lại vào OK trong hộp thoại xuất hiện.

368f17a974c75c73.png

  1. Cuối cùng, hãy cài đặt Dịch vụ Google Play bằng cách chuyển đến thẻ SDK Tools (Bộ công cụ SDK), chọn hộp đánh dấu bên cạnh Dịch vụ Google Play, nhấp vào OK, sau đó chọn OK một lần nữa trong hộp thoại xuất hiện**.**

497a954b82242f4b.png

Các API bắt buộc

Trong Bước 3 của phần sau, hãy bật SDK Maps dành cho AndroidPlaces API cho lớp học lập trình này.

Bắt đầu sử dụng Nền tảng Google Maps

Nếu bạn chưa từng sử dụng Nền tảng Google Maps, hãy làm theo hướng dẫn Bắt đầu sử dụng Nền tảng Google Maps hoặc xem danh sách phát Bắt đầu sử dụng Nền tảng Google Maps để hoàn tất các bước sau:

  1. Tạo tài khoản thanh toán.
  2. Tạo dự án.
  3. Bật các API và SDK của Nền tảng Google Maps (được liệt kê trong phần trước).
  4. Tạo khoá API.

Không bắt buộc: Trình mô phỏng Android

Nếu không có thiết bị hỗ trợ ARCore, bạn có thể sử dụng Trình mô phỏng Android để mô phỏng cảnh thực tế tăng cường cũng như giả mạo vị trí của thiết bị. Vì bạn cũng sẽ sử dụng Sceneform trong bài tập này, nên bạn cũng cần đảm bảo làm theo các bước trong phần "Định cấu hình trình mô phỏng để hỗ trợ Sceneform".

3. Bắt đầu nhanh

Để giúp bạn bắt đầu nhanh nhất có thể, sau đây là một số mã khởi đầu giúp bạn theo dõi lớp học lập trình này. Bạn có thể chuyển ngay đến giải pháp, nhưng nếu muốn xem tất cả các bước, hãy tiếp tục đọc.

Bạn có thể sao chép kho lưu trữ nếu đã cài đặt git.

git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git

Ngoài ra, bạn có thể nhấp vào nút bên dưới để tải mã nguồn xuống.

Sau khi nhận được mã, hãy mở dự án trong thư mục starter.

4. Tổng quan dự án

Khám phá mã bạn đã tải xuống ở bước trước. Trong kho lưu trữ này, bạn sẽ thấy một mô-đun duy nhất có tên là app, chứa gói com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

Các thuộc tính sau được khai báo trong tệp AndroidManifest.xml để cho phép bạn sử dụng các tính năng bắt buộc trong lớp học lập trình này:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature
   android:glEsVersion="0x00030000"
   android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

Đối với uses-permission, chỉ định những quyền mà người dùng cần cấp trước khi có thể sử dụng các chức năng đó, bạn cần khai báo những quyền sau:

  • android.permission.INTERNET – để ứng dụng của bạn có thể thực hiện các thao tác mạng và tìm nạp dữ liệu qua Internet, chẳng hạn như thông tin về địa điểm thông qua Places API.
  • android.permission.CAMERA – bạn cần cấp quyền truy cập camera để có thể dùng camera của thiết bị hiển thị các đối tượng ở chế độ thực tế tăng cường.
  • android.permission.ACCESS_FINE_LOCATION – bạn cần có quyền truy cập vào vị trí để có thể tìm nạp những địa điểm ở gần so với vị trí của thiết bị.

Đối với uses-feature, chỉ định những tính năng phần cứng mà ứng dụng này cần, bạn phải khai báo những nội dung sau:

  • Bạn phải dùng OpenGL ES phiên bản 3.0.
  • Bạn phải có thiết bị hỗ trợ ARCore.

Ngoài ra, các thẻ siêu dữ liệu sau đây sẽ được thêm vào đối tượng ứng dụng:

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">
  
  <!-- 
     Indicates that this app requires Google Play Services for AR ("AR Required") and causes
     the Google Play Store to download and install Google Play Services for AR along with
     the app. For an "AR Optional" app, specify "optional" instead of "required". 
  -->

  <meta-data
     android:name="com.google.ar.core"
     android:value="required" />

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="@string/google_maps_key" />

  <!-- Additional elements here --> 

</application>

Mục siêu dữ liệu đầu tiên là để cho biết rằng ARCore là một yêu cầu để ứng dụng này chạy và mục thứ hai là cách bạn cung cấp khoá API Google Maps Platform cho Maps SDK cho Android.

build.gradle

Trong build.gradle, các phần phụ thuộc bổ sung sau đây được chỉ định:

dependencies {
    // Maps & Location
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.maps.android:maps-utils-ktx:1.7.0'

    // ARCore
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.15.0"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.7.1"
    implementation "com.squareup.retrofit2:converter-gson:2.7.1"
}

Sau đây là nội dung mô tả ngắn gọn về từng phần phụ thuộc:

  • Các thư viện có mã nhóm com.google.android.gms, cụ thể là play-services-locationplay-services-maps, được dùng để truy cập thông tin vị trí của thiết bị và truy cập chức năng liên quan đến Google Maps.
  • com.google.maps.android:maps-utils-ktxthư viện tiện ích Kotlin (KTX) cho Thư viện tiện ích Maps SDK cho Android. Chức năng này sẽ được dùng trong thư viện này để sau này định vị các đối tượng ảo trong không gian thực.
  • com.google.ar.sceneform.ux:sceneform-ux là thư viện Sceneform. Thư viện này sẽ cho phép bạn kết xuất các cảnh 3D chân thực mà không cần phải tìm hiểu về OpenGL.
  • Các phần phụ thuộc trong mã nhận dạng nhóm com.squareup.retrofit2 là các phần phụ thuộc Retrofit. Các phần phụ thuộc này cho phép bạn nhanh chóng viết một ứng dụng HTTP để tương tác với Places API.

Cấu trúc dự án

Tại đây, bạn sẽ thấy các gói và tệp sau:

  • **api** – gói này chứa các lớp dùng để tương tác với Places API bằng Retrofit.
  • **ar** – gói này chứa tất cả các tệp liên quan đến ARCore.
  • **model** – gói này chứa một lớp dữ liệu duy nhất Place, dùng để đóng gói một địa điểm duy nhất do Places API trả về.
  • MainActivity.kt – Đây là Activity duy nhất có trong ứng dụng của bạn, sẽ hiển thị một bản đồ và một khung hiển thị camera.

5. Thiết lập cảnh

Khám phá các thành phần cốt lõi của ứng dụng, bắt đầu từ các thành phần thực tế tăng cường.

MainActivity chứa SupportMapFragment (xử lý việc hiển thị đối tượng bản đồ) và một lớp con của ArFragmentPlacesArFragment – (xử lý việc hiển thị cảnh thực tế tăng cường).

Thiết lập thực tế tăng cường

Ngoài việc hiển thị cảnh thực tế tăng cường, PlacesArFragment cũng sẽ xử lý yêu cầu cấp quyền truy cập camera từ người dùng nếu chưa được cấp. Bạn cũng có thể yêu cầu thêm quyền bằng cách ghi đè phương thức getAdditionalPermissions. Vì bạn cũng cần được cấp quyền truy cập vào vị trí, hãy chỉ định quyền đó và ghi đè phương thức getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

   override fun getAdditionalPermissions(): Array<String> =
       listOf(Manifest.permission.ACCESS_FINE_LOCATION)
           .toTypedArray()
}

Chạy ứng dụng

Tiếp tục mở mã khung trong thư mục starter trong Android Studio. Nếu nhấp vào Run (Chạy) > Run ‘app' (Chạy "ứng dụng") trên thanh công cụ và triển khai ứng dụng cho thiết bị hoặc trình mô phỏng, trước tiên, bạn sẽ được nhắc bật quyền truy cập vào vị trí và camera. Hãy nhấp vào Cho phép. Sau khi nhấp, bạn sẽ thấy chế độ xem camera và chế độ xem bản đồ xuất hiện song song như sau:

e3e3073d5c86f427.png

Phát hiện máy bay

Khi dùng camera để quan sát môi trường xung quanh, bạn có thể nhận thấy một vài chấm trắng xuất hiện trên các bề mặt ngang, giống như các chấm trắng trên thảm trong hình ảnh này.

2a9b6ea7dcb2e249.png

Những chấm trắng này là các đường hướng dẫn do ARCore cung cấp để cho biết rằng một mặt phẳng ngang đã được phát hiện. Các mặt phẳng được phát hiện này cho phép bạn tạo một "điểm neo" để có thể đặt các đối tượng ảo vào không gian thực.

Để biết thêm thông tin về ARCore và cách ARCore nhận biết môi trường xung quanh bạn, hãy đọc về các khái niệm cơ bản của ARCore.

6. Tìm địa điểm lân cận

Tiếp theo, bạn cần truy cập và hiển thị vị trí hiện tại của thiết bị, sau đó tìm nạp các địa điểm lân cận bằng Places API.

Thiết lập Maps

Khoá API Nền tảng Google Maps

Trước đó, bạn đã tạo một khoá API Nền tảng Google Maps để cho phép truy vấn Places API và có thể sử dụng Maps SDK cho Android. Tiếp theo, hãy mở tệp gradle.properties rồi thay thế chuỗi "YOUR API KEY HERE" bằng khoá API mà bạn đã tạo.

Hiển thị vị trí của thiết bị trên bản đồ

Sau khi thêm khoá API, hãy thêm một đối tượng hỗ trợ vào bản đồ để giúp người dùng định hướng vị trí của họ so với bản đồ. Để làm như vậy, hãy chuyển đến phương thức setUpMaps và bên trong lệnh gọi mapFragment.getMapAsync, hãy đặt googleMap.isMyLocationEnabled thành true.. Thao tác này sẽ cho thấy dấu chấm màu xanh dương trên bản đồ.

private fun setUpMaps() {
   mapFragment.getMapAsync { googleMap ->
       googleMap.isMyLocationEnabled = true
       // ...
   }
}

Lấy vị trí hiện tại

Để lấy vị trí của thiết bị, bạn cần sử dụng lớp FusedLocationProviderClient. Việc lấy một thực thể của đối tượng này đã được thực hiện trong phương thức onCreate của MainActivity. Để sử dụng đối tượng này, hãy điền vào phương thức getCurrentLocation. Phương thức này chấp nhận một đối số lambda để có thể truyền vị trí cho phương thức gọi của phương thức này.

Để hoàn tất phương thức này, bạn có thể truy cập vào thuộc tính lastLocation của đối tượng FusedLocationProviderClient, sau đó thêm addOnSuccessListener như sau:

fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    currentLocation = location
    onSuccess(location)
}.addOnFailureListener {
    Log.e(TAG, "Could not get location")
}

Phương thức getCurrentLocation được gọi từ trong hàm lambda được cung cấp trong getMapAsync trong phương thức setUpMaps mà từ đó các địa điểm lân cận được tìm nạp.

Bắt đầu cuộc gọi mạng lưới địa điểm

Trong lệnh gọi phương thức getNearbyPlaces, hãy lưu ý rằng các tham số sau được truyền vào phương thức placesServices.nearbyPlaces – khoá API, vị trí của thiết bị, bán kính tính bằng mét (được đặt thành 2 km) và loại địa điểm (hiện được đặt thành park).

val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
   apiKey = apiKey,
   location = "${location.latitude},${location.longitude}",
   radiusInMeters = 2000,
   placeType = "park"
)

Để hoàn tất lệnh gọi mạng, hãy chuyển khoá API mà bạn đã xác định trong tệp gradle.properties. Đoạn mã sau được xác định trong tệp build.gradle trong cấu hình android > defaultConfig:

android {
   defaultConfig {
       resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
   }
}

Thao tác này sẽ giúp giá trị tài nguyên chuỗi google_maps_key có sẵn tại thời điểm tạo.

Để hoàn tất lệnh gọi mạng, bạn chỉ cần đọc tài nguyên chuỗi này thông qua getString trên đối tượng Context.

val apiKey = this.getString(R.string.google_maps_key)

7. Địa điểm ở chế độ thực tế tăng cường

Cho đến nay, bạn đã thực hiện những việc sau:

  1. Yêu cầu người dùng cấp quyền truy cập camera và vị trí khi chạy ứng dụng lần đầu
  2. Thiết lập ARCore để bắt đầu theo dõi các mặt phẳng ngang
  3. Thiết lập Maps SDK bằng khoá API
  4. Đã nhận được vị trí hiện tại của thiết bị
  5. Tìm nạp các địa điểm lân cận (cụ thể là công viên) bằng Places API

Bước còn lại để hoàn tất bài tập này là định vị những địa điểm mà bạn đang tìm nạp trong thực tế tăng cường.

Tìm hiểu về cảnh

ARCore có thể hiểu cảnh trong thế giới thực thông qua camera của thiết bị bằng cách phát hiện các điểm thú vị và riêng biệt (gọi là điểm đặc trưng) trong mỗi khung hình. Khi các điểm đặc trưng này được nhóm lại và xuất hiện trên một mặt phẳng ngang chung (chẳng hạn như bàn và sàn nhà), ARCore có thể cung cấp tính năng này cho ứng dụng dưới dạng một mặt phẳng ngang.

Như bạn đã thấy trước đó, ARCore giúp hướng dẫn người dùng khi phát hiện thấy một mặt phẳng bằng cách hiển thị các dấu chấm màu trắng.

2a9b6ea7dcb2e249.png

Thêm nút neo

Sau khi phát hiện thấy một mặt phẳng, bạn có thể đính kèm một đối tượng gọi là neo. Thông qua một điểm neo, bạn có thể đặt các đối tượng ảo và đảm bảo rằng các đối tượng đó sẽ xuất hiện ở cùng một vị trí trong không gian. Hãy sửa đổi mã để đính kèm một mã sau khi phát hiện thấy máy bay.

Trong setUpAr, OnTapArPlaneListener được đính kèm vào PlacesArFragment. Trình nghe này được gọi mỗi khi một mặt phẳng được nhấn vào trong cảnh thực tế tăng cường. Trong lệnh gọi này, bạn có thể tạo một Anchor và một AnchorNode từ HitResult được cung cấp trong trình nghe như sau:

arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
   val anchor = hitResult.createAnchor()
   anchorNode = AnchorNode(anchor)
   anchorNode?.setParent(arFragment.arSceneView.scene)
   addPlaces(anchorNode!!)
}

AnchorNode là nơi bạn sẽ đính kèm các đối tượng nút con (các thực thể PlaceNode) trong cảnh được xử lý trong lệnh gọi phương thức addPlaces.

Chạy ứng dụng

Nếu bạn chạy ứng dụng với các nội dung sửa đổi ở trên, hãy quan sát xung quanh cho đến khi phát hiện thấy một mặt phẳng. Hãy nhấn vào các chấm trắng cho biết một mặt phẳng. Sau khi thực hiện thao tác này, bạn sẽ thấy các điểm đánh dấu trên bản đồ cho tất cả các công viên gần bạn nhất. Tuy nhiên, nếu bạn nhận thấy, các đối tượng ảo bị mắc kẹt trên điểm neo đã được tạo và không được đặt tương ứng với vị trí của các công viên đó trong không gian.

f93eb87c98a0098d.png

Trong bước cuối cùng, bạn sẽ khắc phục vấn đề này bằng cách sử dụng Thư viện tiện ích Maps SDK cho AndroidSensorManager trên thiết bị.

8. Định vị địa điểm

Để có thể đặt biểu tượng địa điểm ảo ở chế độ thực tế tăng cường theo hướng chính xác, bạn sẽ cần 2 thông tin sau:

  • Hướng chính bắc
  • Góc giữa hướng bắc và mỗi địa điểm

Xác định hướng bắc

Bạn có thể xác định hướng bắc bằng cách sử dụng các cảm biến vị trí (từ trường và gia tốc kế) có trên thiết bị. Khi dùng 2 cảm biến này, bạn có thể thu thập thông tin theo thời gian thực về vị trí của thiết bị trong không gian. Để biết thêm thông tin về cảm biến vị trí, hãy đọc bài viết Tính toán hướng của thiết bị.

Để truy cập vào các cảm biến này, bạn cần lấy SensorManager rồi đăng ký SensorEventListener trên các cảm biến đó. Các bước này đã được thực hiện cho bạn trong các phương thức vòng đời của MainActivity:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   // ...
   sensorManager = getSystemService()!!
   // ...
}

override fun onResume() {
   super.onResume()
   sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
   sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
}

override fun onPause() {
   super.onPause()
   sensorManager.unregisterListener(this)
}

Trong phương thức onSensorChanged, một đối tượng SensorEvent được cung cấp, trong đó có thông tin chi tiết về dữ liệu của một cảm biến nhất định khi dữ liệu đó thay đổi theo thời gian. Hãy thêm đoạn mã sau vào phương thức đó:

override fun onSensorChanged(event: SensorEvent?) {
   if (event == null) {
       return
   }
   if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
       System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
   } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
       System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
   }

   // Update rotation matrix, which is needed to update orientation angles.
   SensorManager.getRotationMatrix(
       rotationMatrix,
       null,
       accelerometerReading,
       magnetometerReading
   )
   SensorManager.getOrientation(rotationMatrix, orientationAngles)
}

Đoạn mã trên sẽ kiểm tra loại cảm biến và tuỳ thuộc vào loại cảm biến, đoạn mã sẽ cập nhật chỉ số cảm biến thích hợp (chỉ số gia tốc kế hoặc từ kế). Dựa vào các chỉ số của cảm biến này, giờ đây, bạn có thể xác định giá trị của số độ từ hướng bắc so với thiết bị (tức là giá trị của orientationAngles[0]).

Tiêu đề hình cầu

Sau khi xác định hướng bắc, bước tiếp theo là xác định góc giữa hướng bắc và từng địa điểm, sau đó sử dụng thông tin đó để đặt các địa điểm ở tiêu đề chính xác trong thực tế tăng cường.

Để tính hướng đi, bạn sẽ sử dụng Thư viện tiện ích Maps SDK cho Android. Thư viện này chứa một số hàm trợ giúp để tính khoảng cách và hướng đi thông qua hình học cầu. Để biết thêm thông tin, hãy đọc bài tổng quan này về thư viện.

Tiếp theo, bạn sẽ sử dụng phương thức sphericalHeading trong thư viện tiện ích. Phương thức này tính toán hướng/hướng đi giữa 2 đối tượng LatLng. Bạn cần thông tin này trong phương thức getPositionVector được xác định trong Place.kt. Phương thức này cuối cùng sẽ trả về một đối tượng Vector3, sau đó đối tượng này sẽ được mỗi PlaceNode dùng làm vị trí cục bộ trong không gian thực tế tăng cường.

Tiếp tục thay thế định nghĩa tiêu đề trong phương thức đó bằng nội dung sau:

val heading = latLng.sphericalHeading(placeLatLng)

Khi đó, bạn sẽ có định nghĩa phương thức sau:

fun Place.getPositionVector(azimuth: Float, latLng: LatLng): Vector3 {
   val placeLatLng = this.geometry.location.latLng
   val heading = latLng.sphericalHeading(placeLatLng)
   val r = -2f
   val x = r * sin(azimuth + heading).toFloat()
   val y = 1f
   val z = r * cos(azimuth + heading).toFloat()
   return Vector3(x, y, z)
}

Vị trí cục bộ

Bước cuối cùng để định hướng chính xác các địa điểm trong thực tế tăng cường là sử dụng kết quả của getPositionVector khi các đối tượng PlaceNode được thêm vào cảnh. Tiếp tục chuyển đến addPlaces trong MainActivity, ngay bên dưới dòng đặt phần tử mẹ trên mỗi placeNode (ngay bên dưới placeNode.setParent(anchorNode)). Đặt localPosition của placeNode thành kết quả của việc gọi getPositionVector như sau:

val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)

Theo mặc định, phương thức getPositionVector đặt khoảng cách y của nút thành 1 mét theo quy định của giá trị y trong phương thức getPositionVector. Nếu bạn muốn điều chỉnh khoảng cách này (ví dụ: thành 2 mét), hãy sửa đổi giá trị đó nếu cần.

Với thay đổi này, các đối tượng PlaceNode được thêm hiện sẽ được định hướng theo tiêu đề chính xác. Bây giờ, hãy chạy ứng dụng để xem kết quả!

9. Xin chúc mừng

Chúc mừng bạn đã đi được đến đây!

Tìm hiểu thêm