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ủa bạn có thể quản lý nhiều người dùng Android trên các 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 khi chạy trên thiết bị được quản lý hoàn toàn có thể tạo và quản lý hai kiểu người dùng:
- Người dùng phụ là những người dùng Android đã lưu dữ liệu và ứng dụng riêng biệt 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 hữu ích cho trường hợp người dùng nhấc thiết bị lên khi bắt đầu ca làm việc, chẳng hạn như tài xế giao hàng hoặc nhân viên an ninh.
- Người dùng tạm thời là những 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 trong đó dữ liệu có thể bị xoá sau khi phiên kết thúc, chẳng hạn như quyền truy cập công khai ki-ốt internet.
Bạn sử dụng DPC hiện có để quản lý thiết bị chuyên dụng và người dùng. Một thành phần quản trị trong DPC của bạn sẽ tự đặt làm quản trị viên cho phụ mới khi tạo chúng.
Quản trị viên của người dùng phụ phải thuộc cùng gói với quản trị viên của thiết bị được quản lý toàn bộ. Để đơn giản hoá quá trình phát triển, bạn nên chia sẻ thông tin về quản trị viên 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 pháp được sử dụng trong hướng dẫn của nhà phát triển này có sẵn trong các phiên bản Android cũ hơn.
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 mạng mới. Tuy nhiên, chúng không thể chỉnh sửa hoặc xóa mạng, ngay cả những mạng mà các mạng đó đã tạo.
Tạo người dùng
DPC của bạn có thể tạo thêm người dùng trong nền và sau đó có thể chuyển đổi họ lên nền trước. Quá trình này gần giống nhau đối với cả phụ và người dùng tạm thời. Thực hiện các bước sau đây trong phần quản trị của thiết bị được quản lý và người dùng phụ:
- Gọi
DevicePolicyManager.createAndManageUser()
. Để tạo người dùng tạm thời, hãy thêmMAKE_USER_EPHEMERAL
trong đối số cờ. - Gọi điện
DevicePolicyManager.startUserInBackground()
đến khởi động người dùng trong 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 thị cho người đang sử dụng thiết bị. - Trong quản trị viên của người dùng phụ, hãy gọi
DevicePolicyManager.setAffiliationIds()
đến liên kết người dùng mới với người dùng chính. Xem Phối hợp DPC bên dưới. - Quay lại phần 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 trình bày cách thêm bước 1 vào DPC:
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 dẫn đến lỗi
bằng cách phát hiện ngoại lệ UserOperationException
và gọi
getUserOperationResult()
. Vượt quá người dùng
hạn mức là các 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 trong nền. Bạn có thể cần cân bằng lợi thế của người dùng đã sẵn sàng với việc số người dùng đượ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 này bằng một sê-ri cố định
số. Đừng duy trì UserHandle
vì hệ thống sẽ tái chế những 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 điện
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 của 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 chế độ thiết lập
người dùng. Bạn có thể thêm các cờ sau đây khi gọi createAndManageUser()
:
SKIP_SETUP_WIZARD
- Bỏ qua việc chạy trình hướng dẫn thiết lập người dùng mới để 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à thiết lập một phương thức khoá màn hình. Việc này có thể mất chút thời gian và có thể không áp dụng cho tất cả người dùng—ví dụ: kiosk Internet công cộng.
LEAVE_ALL_SYSTEM_APPS_ENABLED
- Luôn bật tất cả ứng dụng hệ thống trong người dùng mới. Nếu không đặt cờ này, người dùng mới chỉ chứa tập hợp ứng dụng tối thiểu mà điện thoại cần hoạt động—thường là một 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ý hoàn toàn) có thể thấy hữu ích khi
biết khi nào người dùng thứ hai 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ể tải người dùng này qua
startedUser
đối số. onUserSwitched()
- Được gọi sau khi hệ thống chuyển sang một người dùng khác. Bạn có thể thu hút 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
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ể tải
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ụ
thiết lập đầy đủ khi DPC của bạn nhận 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 đó đã bị xoá,
bạn không thể truy cập vào người dùng mà đối số
removedUser
đại diện.
Để biết khi nào hệ thống đưa người dùng lên nền trước hoặc gửi người dùng đến
ở chế độ nền, ứng dụng có thể đăng ký dịch vụ nhận cho
ACTION_USER_FOREGROUND
và
ACTION_USER_BACKGROUND
thông báo.
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 diện có thể gọi
DevicePolicyManager.getSecondaryUsers()
. Kết quả
bao gồm mọi người dùng phụ hoặc người dùng tạm thời do quản trị viên tạo. Kết quả cũng
bao gồm bất kỳ người dùng phụ nào (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ì chúng không phải là
người dùng phụ. Mẫu sau đây cho thấy 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à các 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 một người dùng phụ để tìm hiểu xem đây có phải là người dùng tạm thời.
DevicePolicyManager.isAffiliatedUser()
- Gọi phương thức này từ quản trị viên của một 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. Để tìm hiểu thêm về đơn vị liên kết, hãy xem 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 cho 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ể xóa người dùng khi thiết bị không được sử dụng trong một khoảng thời gian hoặc bạn có thể gửi mọi đơn đặt hàng chưa được gửi tới máy chủ trước khi ca của một người kết thúc.
Đăng xuất
Android 9.0 thêm một nút đăng xuất vào màn hình khoá để người dùng thiết bị nào có thể kết thúc phiên của họ. Sau khi người dùng 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 người dùng tạm thời và người dùng chính quay lại lên nền trước. Android ẩn nút này khi người dùng chính đang ở ở chế độ 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 kết thúc phiên nhưng hiển thị quản trị viên (của
thiết bị được quản lý hoàn toàn) có thể bật tính năng 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, 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 rồi quay lại theo phương thức lập trình
đối với người dùng chính. Trước tiên, hãy xác nhận người dùng phụ và người dùng chính là
đượ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 người dùng phụ khác, quản trị viên của một thiết bị được quản lý toàn diện 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 người dùng phụ, DPC sở hữu thiết bị được quản lý hoàn toàn có thể gọi
DevicePolicyManager.stopUser()
. Nếu người dùng bị dừng là
người dùng tạm thời, người dùng sẽ dừng lại, sau đó bị xoá.
Bất cứ khi nào có thể, bạn nên dừng người dùng để giúp thiết bị không vượt quá số người dùng đang chạy tối đa.
Xóa người dùng
Để xoá vĩnh viễn người dùng phụ, DPC có thể gọi một trong các lệnh sau
DevicePolicyManager
phương thức:
- Quản trị viên của một thiết bị được quản lý toàn diện có thể gọi hàm
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, dừng hoặc chuyển đổi xa.
Vô hiệu hoá giao diện người dùng mặc định
Nếu DPC của bạn cung cấp giao diện người dùng để quản lý người dùng, bạn có thể tắt các tính năng
giao diện nhiều người dùng. Bạn có thể thực hiện việc này bằng cách gọi
DevicePolicyManager.setLogoutEnabled()
rồi thêm tham số
DISALLOW_USER_SWITCH
như minh hoạ 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
Giới hạn người dùng DISALLOW_ADD_USER
.
Thông báo trong phiên
Khi một người đang sử dụng một thiết bị chuyển sang một người dùng mới, Android sẽ hiển thị một bảng điều khiển để đánh dấu nút chuyển. Android hiện các thông báo sau:
- Thông báo bắt đầu phiên hoạt động của người dùng xuất hiện khi thiết bị chuyển sang người dùng khỏi người dùng chính.
- Thông báo trong phiên hoạt động của người dùng cuối xuất hiện khi thiết bị quay lại người dùng chính từ một người dùng phụ.
Hệ thống sẽ không hiện thông báo khi chuyển đổi giữa hai người dùng phụ.
Do các thông báo có thể không phù hợp với mọi tình huống nên bạn có thể thay đổi nội dung của các thư này. Ví dụ: nếu giải pháp của bạn sử dụng 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 trình duyệt buổi tập & đang xoá dữ liệu cá nhân...
Hệ thống hiển thị thông báo chỉ trong vài giây, nên mỗi thông báo
phải là một cụm từ ngắn gọn, rõ ràng. Để tuỳ chỉnh tin nhắn, quản trị viên có thể gọi
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á thông điệp tuỳ chỉnh của bạn và quay lại chế độ mặc định của Android
tin nhắn. 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á cho 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 thay đổi ngôn ngữ của người dùng:
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 đến hai phiên bản DPC, trong đó một phiên bản sở hữu thiết bị được quản lý toàn bộ trong khi thiết bị còn lại sở hữu người dùng phụ. Khi tạo một người dùng mới, quản trị viên của thiết bị được quản lý toàn bộ sẽ đặt một phiên bản khác của bản thân nó dưới vai trò 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 của nhà phát triển này chỉ hoạt động khi người dùng phụ được liên kết. Vì Android tắt 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. Xem ví dụ trong 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 đó
cho phép mọi người sử dụng chúng. Bạn có thể thực hiện thiết lập này từ
Lệnh gọi lại DeviceAdminReceiver.onEnabled()
. Nếu trước đây
đặt bất kỳ phần bổ sung quản trị nào trong lệnh gọi tới createAndManageUser()
, bạn có thể lấy hàm
các giá trị từ đối số intent
. Ví dụ sau đây thể hiện một đơn vị liên kết DPC
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ù hai phiên bản DPC đang chạy dưới những người dùng riêng biệt, DPC
sở hữu thiết bị và người dùng phụ có thể giao tiếp với nhau.
Do việc gọi dịch vụ của DPC khác vượt qua ranh giới của người dùng, DPC của bạn không thể
gọi bindService()
như bạn thường làm
Android. Để liên kết với một dịch vụ đang chạy trong
một 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 người dùng được trả về bởi
DevicePolicyManager.getBindDeviceAdminTargetUsers()
.
Ví dụ sau đây cho thấy quản trị viên của một người dùng phụ liên kết 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 về thiết bị chuyên dụng với các ví dụ khác để hạn chế các thiết bị chuyên dụng và cải thiện trải nghiệm người dùng của bạn.