Hướng dẫn dành cho nhà phát triển này giải thích cách trình kiểm soát chính sách thiết bị (DPC) có thể quản lý nhiều người dùng Android trên thiết bị chuyên dụng.
Tổng quan
DPC của bạn có thể giúp nhiều người dùng chung một thiết bị chuyên dụng. DPC của bạn chạy trên một thiết bị được quản lý toàn bộ có thể tạo và quản lý 2 loại người dùng:
- Người dùng phụ là người dùng Android có các ứng dụng và dữ liệu riêng biệt được lưu giữa các phiên. Bạn quản lý người dùng bằng thành phần quản trị viên. Những người dùng này rất hữu ích trong các trường hợp thiết bị được đến khi bắt đầu ca làm việc, chẳng hạn như trình điều khiển phân phối hoặc nhân viên bảo mật.
- Người dùng tạm thời là người dùng phụ mà hệ thống sẽ xoá khi người dùng dừng, chuyển đi hoặc thiết bị khởi động lại. Những người dùng này hữu ích cho các trường hợp dữ liệu có thể bị xoá sau khi phiên kết thúc, chẳng hạn như các quầy Internet truy cập công cộng.
Bạn sẽ sử dụng DPC hiện có để quản lý thiết bị chuyên dụng và người dùng phụ. Một thành phần quản trị trong DPC sẽ tự đặt mình làm quản trị viên cho người dùng phụ mới khi bạn tạo họ.
Quản trị viên của người dùng phụ phải thuộc cùng một gói với quản trị viên của thiết bị được quản lý hoàn toàn. Để đơn giản hoá quá trình phát triển, bạn nên chia sẻ quyền quản trị giữa thiết bị và người dùng phụ.
Việc quản lý nhiều người dùng trên các thiết bị chuyên dụng thường yêu cầu Android 9.0, tuy nhiên một số phương thức dùng trong hướng dẫn của nhà phát triển này lại có trong các phiên bản Android cũ.
Người dùng phụ
Người dùng phụ có thể kết nối với Wi-Fi và có thể định cấu hình các mạng mới. Tuy nhiên, họ không thể chỉnh sửa hoặc xoá mạng, thậm chí không thể xoá mạng mà họ đã tạo.
Tạo người dùng
DPC của bạn có thể tạo thêm người dùng ở chế độ nền, sau đó chuyển họ sang nền trước. Quy trình này gần như giống nhau đối với cả người dùng phụ và người dùng tạm thời. Triển khai các bước sau đây trong phần quản trị của thiết bị được quản lý hoàn toàn và của người dùng phụ:
- Gọi
DevicePolicyManager.createAndManageUser()
. Để tạo người dùng tạm thời, hãy đưaMAKE_USER_EPHEMERAL
vào đối số cờ. - Gọi
DevicePolicyManager.startUserInBackground()
để bắt đầu người dùng ở chế độ nền. Người dùng bắt đầu chạy nhưng bạn muốn hoàn tất quá trình thiết lập trước khi đưa người dùng lên nền trước và hiện ứng dụng đó cho người dùng thiết bị. - Trong bảng điều khiển quản trị của người dùng phụ, hãy gọi
DevicePolicyManager.setAffiliationIds()
để liên kết người dùng mới với người dùng chính. Xem phần Phối hợp với DPC ở bên dưới. - Quay lại trang quản trị của thiết bị được quản lý toàn bộ, gọi
DevicePolicyManager.switchUser()
để chuyển người dùng sang nền trước.
Mẫu sau đây cho biết cách bạn có thể thêm bước 1 vào DPC của mình:
Kotlin
val dpm = getContext().getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager // If possible, reuse an existing affiliation ID across the // primary user and (later) the ephemeral user. val identifiers = dpm.getAffiliationIds(adminName) if (identifiers.isEmpty()) { identifiers.add(UUID.randomUUID().toString()) dpm.setAffiliationIds(adminName, identifiers) } // Pass an affiliation ID to the ephemeral user in the admin extras. val adminExtras = PersistableBundle() adminExtras.putString(AFFILIATION_ID_KEY, identifiers.first()) // Include any other config for the new user here ... // Create the ephemeral user, using this component as the admin. try { val ephemeralUser = dpm.createAndManageUser( adminName, "tmp_user", adminName, adminExtras, DevicePolicyManager.MAKE_USER_EPHEMERAL or DevicePolicyManager.SKIP_SETUP_WIZARD) } catch (e: UserManager.UserOperationException) { if (e.userOperationResult == UserManager.USER_OPERATION_ERROR_MAX_USERS) { // Find a way to free up users... } }
Java
DevicePolicyManager dpm = (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE); // If possible, reuse an existing affiliation ID across the // primary user and (later) the ephemeral user. Set<String> identifiers = dpm.getAffiliationIds(adminName); if (identifiers.isEmpty()) { identifiers.add(UUID.randomUUID().toString()); dpm.setAffiliationIds(adminName, identifiers); } // Pass an affiliation ID to the ephemeral user in the admin extras. PersistableBundle adminExtras = new PersistableBundle(); adminExtras.putString(AFFILIATION_ID_KEY, identifiers.iterator().next()); // Include any other config for the new user here ... // Create the ephemeral user, using this component as the admin. try { UserHandle ephemeralUser = dpm.createAndManageUser( adminName, "tmp_user", adminName, adminExtras, DevicePolicyManager.MAKE_USER_EPHEMERAL | DevicePolicyManager.SKIP_SETUP_WIZARD); } catch (UserManager.UserOperationException e) { if (e.getUserOperationResult() == UserManager.USER_OPERATION_ERROR_MAX_USERS) { // Find a way to free up users... } }
Khi tạo hoặc bắt đầu một người dùng mới, bạn có thể kiểm tra lý do của mọi lỗi bằng cách phát hiện trường hợp ngoại lệ UserOperationException
và gọi getUserOperationResult()
. Vượt quá giới hạn người dùng là những lý do phổ biến gây ra lỗi:
Quá trình tạo người dùng có thể mất chút thời gian. Nếu thường xuyên tạo người dùng, bạn có thể cải thiện trải nghiệm người dùng bằng cách chuẩn bị một người dùng sẵn sàng sử dụng ở chế độ nền. Bạn có thể cần cân bằng các ưu điểm của người dùng sẵn sàng sử dụng với số lượng người dùng tối đa được phép trên thiết bị.
Phát triển mối đồng cảm
Sau khi tạo người dùng mới, bạn nên tham chiếu đến người dùng bằng một số sê-ri cố định. Không duy trì UserHandle
vì hệ thống sẽ tái chế các thành phần này khi bạn tạo và xoá người dùng. Lấy số sê-ri bằng cách gọi UserManager.getSerialNumberForUser()
:
Kotlin
// After calling createAndManageUser() use a device-unique serial number // (that isn’t recycled) to identify the new user. secondaryUser?.let { val userManager = getContext().getSystemService(UserManager::class.java) val ephemeralUserId = userManager!!.getSerialNumberForUser(it) // Save the serial number to storage ... }
Java
// After calling createAndManageUser() use a device-unique serial number // (that isn’t recycled) to identify the new user. if (secondaryUser != null) { UserManager userManager = getContext().getSystemService(UserManager.class); long ephemeralUserId = userManager.getSerialNumberForUser(secondaryUser); // Save the serial number to storage ... }
Cấu hình người dùng
Tuỳ thuộc vào nhu cầu của người dùng, bạn có thể tuỳ chỉnh cách thiết lập người dùng phụ. Bạn có thể đưa vào các cờ sau khi gọi createAndManageUser()
:
SKIP_SETUP_WIZARD
- Bỏ qua bước chạy trình hướng dẫn thiết lập người dùng mới nhằm kiểm tra và cài đặt bản cập nhật, nhắc người dùng thêm Tài khoản Google cùng với các dịch vụ của Google và đặt phương thức khoá màn hình. Quá trình này có thể mất chút thời gian và có thể không áp dụng được cho một số người dùng, chẳng hạn như các kiosk Internet công cộng.
LEAVE_ALL_SYSTEM_APPS_ENABLED
- Bật chế độ bật tất cả các ứng dụng hệ thống trong chế độ người dùng mới. Nếu bạn không đặt cờ này, người dùng mới sẽ chỉ chứa nhóm ứng dụng tối thiểu mà điện thoại cần để hoạt động – thường là trình duyệt tệp, trình quay số điện thoại, danh bạ và tin nhắn SMS.
Theo dõi vòng đời của người dùng
DPC của bạn (nếu là quản trị viên của thiết bị được quản lý toàn bộ) có thể thấy hữu ích khi biết thời điểm người dùng phụ thay đổi. Để chạy các tác vụ tiếp theo sau khi thay đổi, hãy ghi đè các phương thức gọi lại này trong lớp con DeviceAdminReceiver
của DPC:
onUserStarted()
- Được gọi sau khi hệ thống khởi động một người dùng. Người dùng này có thể vẫn đang thiết lập hoặc đang chạy trong nền. Bạn có thể lấy người dùng từ đối số
startedUser
. onUserSwitched()
- Được gọi sau khi hệ thống chuyển sang một người dùng khác. Bạn có thể lấy người dùng mới
hiện đang chạy ở nền trước từ đối số
switchedUser
. onUserStopped()
- Được gọi sau khi hệ thống dừng một người dùng vì họ đã đăng xuất, chuyển sang một người dùng mới (nếu người dùng tạm thời) hoặc DPC của bạn đã dừng người dùng. Bạn có thể lấy người dùng qua đối số
stoppedUser
. onUserAdded()
- Được gọi khi hệ thống thêm một người dùng mới. Thông thường, người dùng phụ không được thiết lập đầy đủ khi DPC nhận được lệnh gọi lại. Bạn có thể lấy người dùng từ đối số
newUser
. onUserRemoved()
- Được gọi sau khi hệ thống xoá một người dùng. Vì người dùng này đã bị xoá, nên bạn không thể truy cập vào người dùng được đại diện bằng đối số
removedUser
.
Để biết thời điểm hệ thống đưa người dùng lên nền trước hoặc chuyển người dùng sang chế độ nền, các ứng dụng có thể đăng ký bộ thu cho các thông báo ACTION_USER_FOREGROUND
và ACTION_USER_BACKGROUND
.
Khám phá người dùng
Để có tất cả người dùng phụ, quản trị viên của thiết bị được quản lý toàn bộ có thể gọi DevicePolicyManager.getSecondaryUsers()
. Kết quả bao gồm mọi người dùng phụ hoặc tạm thời do quản trị viên tạo. Kết quả cũng bao gồm mọi người dùng phụ (hoặc người dùng khách) mà một người sử dụng thiết bị có thể đã tạo. Kết quả không bao gồm hồ sơ công việc vì họ không phải là người dùng phụ. Mẫu sau đây cho biết cách bạn có thể sử dụng phương thức này:
Kotlin
// The device is stored for the night. Stop all running secondary users. dpm.getSecondaryUsers(adminName).forEach { dpm.stopUser(adminName, it) }
Java
// The device is stored for the night. Stop all running secondary users. for (UserHandle user : dpm.getSecondaryUsers(adminName)) { dpm.stopUser(adminName, user); }
Dưới đây là những phương thức khác mà bạn có thể gọi để tìm hiểu trạng thái của người dùng phụ:
DevicePolicyManager.isEphemeralUser()
- Gọi phương thức này từ quản trị viên của người dùng phụ để tìm hiểu xem đây có phải là người dùng tạm thời hay không.
DevicePolicyManager.isAffiliatedUser()
- Gọi phương thức này từ quản trị viên của người dùng phụ để tìm hiểu xem người dùng này có được liên kết với người dùng chính hay không. Để tìm hiểu thêm về mối liên kết, hãy xem phần điều phối DPC ở bên dưới.
Quản lý người dùng
Nếu muốn quản lý hoàn toàn vòng đời của người dùng, bạn có thể gọi API để kiểm soát chi tiết thời điểm và cách thiết bị thay đổi người dùng. Ví dụ: bạn có thể xoá người dùng khi bạn không sử dụng thiết bị trong một khoảng thời gian hoặc bạn có thể gửi bất kỳ đơn đặt hàng nào chưa gửi tới máy chủ trước khi ca làm việc của người dùng kết thúc.
Đăng xuất
Android 9.0 đã thêm nút đăng xuất vào màn hình khoá để một người sử dụng thiết bị có thể kết thúc phiên của họ. Sau khi nhấn vào nút này, hệ thống sẽ dừng người dùng phụ, xoá người dùng đó nếu là tạm thời và người dùng chính quay lại nền trước. Android ẩn nút này khi người dùng chính ở nền trước vì người dùng chính không thể đăng xuất.
Theo mặc định, Android không hiển thị nút phiên kết thúc, nhưng quản trị viên của bạn (của một thiết bị được quản lý hoàn toàn) có thể bật nút này bằng cách gọi DevicePolicyManager.setLogoutEnabled()
. Nếu bạn cần xác nhận trạng thái hiện tại của nút, hãy gọi DevicePolicyManager.isLogoutEnabled()
.
Quản trị viên của người dùng phụ có thể đăng xuất người dùng và quay lại người dùng chính theo phương thức lập trình. Trước tiên, hãy xác nhận người dùng phụ và người dùng chính đã được liên kết, sau đó gọi DevicePolicyManager.logoutUser()
. Nếu người dùng đăng xuất là người dùng tạm thời, hệ thống sẽ dừng rồi xoá người dùng đó.
Chuyển đổi người dùng
Để chuyển sang một người dùng phụ khác, quản trị viên của thiết bị được quản lý toàn bộ có thể gọi DevicePolicyManager.switchUser()
. Để thuận tiện, bạn có thể truyền null
để chuyển sang người dùng chính.
Dừng một người dùng
Để dừng một người dùng phụ, DPC sở hữu thiết bị được quản lý toàn bộ có thể gọi DevicePolicyManager.stopUser()
. Nếu người dùng bị dừng là người dùng tạm thời, thì người dùng đó sẽ bị dừng rồi bị xoá.
Bạn nên dừng người dùng bất cứ khi nào có thể để giúp duy trì dưới số lượng người dùng đang chạy tối đa của thiết bị.
Xóa người dùng
Để xoá vĩnh viễn một người dùng phụ, DPC có thể gọi một trong các phương thức DevicePolicyManager
sau:
- Quản trị viên của thiết bị được quản lý toàn bộ có thể gọi
removeUser()
. - Quản trị viên của người dùng phụ có thể gọi
wipeData()
.
Hệ thống sẽ xoá người dùng tạm thời khi họ đăng xuất, ngừng hoạt động hoặc rời khỏi ứng dụng.
Tắt giao diện người dùng mặc định
Nếu DPC cung cấp giao diện người dùng để quản lý người dùng, bạn có thể tắt giao diện nhiều người dùng tích hợp sẵn của Android. Bạn có thể thực hiện việc này bằng cách gọi DevicePolicyManager.setLogoutEnabled()
và thêm quy tắc hạn chế DISALLOW_USER_SWITCH
như trong ví dụ sau:
Kotlin
// Explicitly disallow logging out using Android UI (disabled by default). dpm.setLogoutEnabled(adminName, false) // Disallow switching users in Android's UI. This DPC can still // call switchUser() to manage users. dpm.addUserRestriction(adminName, UserManager.DISALLOW_USER_SWITCH)
Java
// Explicitly disallow logging out using Android UI (disabled by default). dpm.setLogoutEnabled(adminName, false); // Disallow switching users in Android's UI. This DPC can still // call switchUser() to manage users. dpm.addUserRestriction(adminName, UserManager.DISALLOW_USER_SWITCH);
Người sử dụng thiết bị không thể thêm người dùng phụ bằng giao diện người dùng tích hợp sẵn của Android vì quản trị viên của các thiết bị được quản lý toàn bộ sẽ tự động thêm quy tắc hạn chế người dùng DISALLOW_ADD_USER
.
Thông báo trong phiên
Khi người dùng thiết bị chuyển sang người dùng mới, Android sẽ hiển thị một bảng điều khiển để làm nổi bật nút chuyển đó. Android sẽ hiện các thông báo sau:
- Thông báo về phiên bắt đầu của người dùng hiển thị khi thiết bị chuyển từ người dùng chính sang người dùng phụ.
- Thông báo về phiên hoạt động của người dùng cuối xuất hiện khi thiết bị của một người dùng phụ quay lại người dùng chính.
Hệ thống không hiển thị thông báo khi chuyển đổi giữa hai người dùng phụ.
Vì các thông báo có thể không phù hợp với một số tình huống nên bạn có thể thay đổi nội dung của các thông báo này. Ví dụ: nếu giải pháp của bạn sử dụng các phiên người dùng tạm thời, bạn có thể phản ánh điều này trong các thông báo như: Dừng phiên trình duyệt và xoá dữ liệu cá nhân...
Hệ thống chỉ hiển thị thông báo trong vài giây, vì vậy, mỗi thông báo phải là một cụm từ ngắn và rõ ràng. Để tuỳ chỉnh thông báo, quản trị viên có thể gọi các phương thức DevicePolicyManager
setStartUserSessionMessage()
và setEndUserSessionMessage()
như minh hoạ trong ví dụ sau:
Kotlin
// Short, easy-to-read messages shown at the start and end of a session. // In your app, store these strings in a localizable resource. internal val START_USER_SESSION_MESSAGE = "Starting guest session…" internal val END_USER_SESSION_MESSAGE = "Stopping & clearing data…" // ... dpm.setStartUserSessionMessage(adminName, START_USER_SESSION_MESSAGE) dpm.setEndUserSessionMessage(adminName, END_USER_SESSION_MESSAGE)
Java
// Short, easy-to-read messages shown at the start and end of a session. // In your app, store these strings in a localizable resource. private static final String START_USER_SESSION_MESSAGE = "Starting guest session…"; private static final String END_USER_SESSION_MESSAGE = "Stopping & clearing data…"; // ... dpm.setStartUserSessionMessage(adminName, START_USER_SESSION_MESSAGE); dpm.setEndUserSessionMessage(adminName, END_USER_SESSION_MESSAGE);
Truyền null
để xoá các thông báo tuỳ chỉnh và quay lại các thông báo mặc định của Android. Nếu bạn cần kiểm tra nội dung tin nhắn hiện tại, hãy gọi getStartUserSessionMessage()
hoặc getEndUserSessionMessage()
.
DPC của bạn phải đặt thông báo đã bản địa hoá theo ngôn ngữ hiện tại của người dùng. Bạn cũng cần cập nhật thông báo khi ngôn ngữ của người dùng thay đổi:
Kotlin
override fun onReceive(context: Context?, intent: Intent?) { // Added the <action android:name="android.intent.action.LOCALE_CHANGED" /> // intent filter for our DeviceAdminReceiver subclass in the app manifest file. if (intent?.action === ACTION_LOCALE_CHANGED) { // Android's resources return a string suitable for the new locale. getManager(context).setStartUserSessionMessage( getWho(context), context?.getString(R.string.start_user_session_message)) getManager(context).setEndUserSessionMessage( getWho(context), context?.getString(R.string.end_user_session_message)) } super.onReceive(context, intent) }
Java
public void onReceive(Context context, Intent intent) { // Added the <action android:name="android.intent.action.LOCALE_CHANGED" /> // intent filter for our DeviceAdminReceiver subclass in the app manifest file. if (intent.getAction().equals(ACTION_LOCALE_CHANGED)) { // Android's resources return a string suitable for the new locale. getManager(context).setStartUserSessionMessage( getWho(context), context.getString(R.string.start_user_session_message)); getManager(context).setEndUserSessionMessage( getWho(context), context.getString(R.string.end_user_session_message)); } super.onReceive(context, intent); }
Điều phối DPC
Việc quản lý người dùng phụ thường cần 2 thực thể của DPC — một thực thể sở hữu thiết bị được quản lý hoàn toàn trong khi bản còn lại sở hữu người dùng phụ. Khi tạo người dùng mới, quản trị viên của thiết bị được quản lý hoàn toàn sẽ đặt một thực thể khác của chính mình làm quản trị viên của người dùng mới.
Người dùng liên kết
Một số API trong hướng dẫn dành cho nhà phát triển này chỉ hoạt động khi người dùng phụ được liên kết. Vì Android vô hiệu hoá một số tính năng (ví dụ: ghi nhật ký mạng) khi bạn thêm người dùng phụ mới chưa liên kết vào thiết bị, bạn nên liên kết người dùng càng sớm càng tốt. Hãy xem ví dụ trong phần Thiết lập bên dưới.
Thiết lập
Thiết lập người dùng phụ mới (từ DPC sở hữu người dùng phụ) trước khi cho phép mọi người sử dụng những người dùng đó. Bạn có thể thực hiện việc thiết lập này thông qua lệnh gọi lại DeviceAdminReceiver.onEnabled()
. Nếu trước đây bạn đã đặt bất kỳ phần bổ sung quản trị nào trong lệnh gọi đến createAndManageUser()
, thì bạn có thể nhận các giá trị từ đối số intent
. Ví dụ sau đây cho thấy một DPC liên kết với một người dùng phụ mới trong lệnh gọi lại:
Kotlin
override fun onEnabled(context: Context?, intent: Intent?) { super.onEnabled(context, intent) // Get the affiliation ID (our DPC previously put in the extras) and // set the ID for this new secondary user. intent?.getStringExtra(AFFILIATION_ID_KEY)?.let { val dpm = getManager(context) dpm.setAffiliationIds(getWho(context), setOf(it)) } // Continue setup of the new secondary user ... }
Java
public void onEnabled(Context context, Intent intent) { // Get the affiliation ID (our DPC previously put in the extras) and // set the ID for this new secondary user. String affiliationId = intent.getStringExtra(AFFILIATION_ID_KEY); if (affiliationId != null) { DevicePolicyManager dpm = getManager(context); dpm.setAffiliationIds(getWho(context), new HashSet<String>(Arrays.asList(affiliationId))); } // Continue setup of the new secondary user ... }
RPC giữa các DPC
Mặc dù 2 thực thể DPC đang chạy dưới những người dùng riêng biệt, nhưng DPC sở hữu thiết bị và người dùng phụ có thể giao tiếp với nhau.
Vì việc gọi dịch vụ của một DPC khác vượt qua ranh giới người dùng, nên DPC của bạn không thể gọi bindService()
như bình thường trong Android. Để liên kết với một dịch vụ đang chạy trong người dùng khác, hãy gọi DevicePolicyManager.bindDeviceAdminServiceAsUser()
.
DPC của bạn chỉ có thể liên kết với các dịch vụ đang chạy trong những người dùng do DevicePolicyManager.getBindDeviceAdminTargetUsers()
trả về.
Ví dụ sau đây cho thấy quản trị viên của liên kết người dùng phụ với quản trị viên của thiết bị được quản lý hoàn toàn:
Kotlin
// From a secondary user, the list contains just the primary user. dpm.getBindDeviceAdminTargetUsers(adminName).forEach { // Set up the callbacks for the service connection. val intent = Intent(mContext, FullyManagedDeviceService::class.java) val serviceconnection = object : ServiceConnection { override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) { // Call methods on service ... } override fun onServiceDisconnected(componentName: ComponentName) { // Clean up or reconnect if needed ... } } // Bind to the service as the primary user [it]. val bindSuccessful = dpm.bindDeviceAdminServiceAsUser(adminName, intent, serviceconnection, Context.BIND_AUTO_CREATE, it) }
Java
// From a secondary user, the list contains just the primary user. List<UserHandle> targetUsers = dpm.getBindDeviceAdminTargetUsers(adminName); if (targetUsers.isEmpty()) { // If the users aren't affiliated, the list doesn't contain any users. return; } // Set up the callbacks for the service connection. Intent intent = new Intent(mContext, FullyManagedDeviceService.class); ServiceConnection serviceconnection = new ServiceConnection() { @Override public void onServiceConnected( ComponentName componentName, IBinder iBinder) { // Call methods on service ... } @Override public void onServiceDisconnected(ComponentName componentName) { // Clean up or reconnect if needed ... } }; // Bind to the service as the primary user. UserHandle primaryUser = targetUsers.get(0); boolean bindSuccessful = dpm.bindDeviceAdminServiceAsUser( adminName, intent, serviceconnection, Context.BIND_AUTO_CREATE, primaryUser);
Tài nguyên khác
Để tìm hiểu thêm về các thiết bị chuyên dụng, hãy đọc các tài liệu sau:
- Tổng quan về thiết bị chuyên dụng là thông tin tổng quan về các thiết bị chuyên dụng.
- Chế độ khoá tác vụ giải thích cách khoá một thiết bị chuyên dụng với một ứng dụng hoặc một nhóm ứng dụng.
- Sổ tay nấu ăn dành cho thiết bị chuyên dụng với nhiều ví dụ khác để hạn chế các thiết bị chuyên dụng và nâng cao trải nghiệm người dùng.