Bỏ qua để đến nội dung

Thuộc tính android:autoVerify

android:autoVerify="true" là một thuộc tính nhỏ trên thẻ <intent-filter>, nhưng là thứ quyết định một link https sẽ mở app hay mở trình duyệt. Bài này đi sâu vào thuộc tính đó; bối cảnh tổng quan về deep link xem ở bài Universal Links và App Links.

Ví dụ: cùng một intent-filter, có và không có autoVerify

Phần tiêu đề “Ví dụ: cùng một intent-filter, có và không có autoVerify”

App MyShop khai báo nhận link https://shop.example.com/products/...:

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="shop.example.com" />
<data android:pathPrefix="/products/" />
</intent-filter>

Website đã đặt https://shop.example.com/.well-known/assetlinks.json đúng. Người dùng bấm link https://shop.example.com/products/123 trong ứng dụng tin nhắn:

Không có autoVerifyCó autoVerify="true", xác minh thành công
Android 12 trở lên (app target 31+)Mở trình duyệt (link https chưa xác minh mặc định mở trình duyệt)Mở thẳng app
Android 6 đến 11Hiện hộp thoại chọn ứng dụng (trình duyệt hoặc MyShop)Mở thẳng app

Không có autoVerify, intent-filter trên chỉ là một deep link thường. Có autoVerify và xác minh thành công, nó trở thành Android App Link: app được hệ thống công nhận là trình xử lý mặc định cho domain đó.

Điểm đáng chú ý: trên Android 12+, thiếu autoVerify hay xác minh thất bại đều không báo lỗi gì, link chỉ lặng lẽ mở trình duyệt. Vì vậy khi link “không mở app”, việc đầu tiên là kiểm tra trạng thái xác minh (xem phần Kiểm tra trạng thái xác minh).

Thuộc tính này báo cho hệ thống: “hãy xác minh rằng tôi thật sự được domain này cho phép mở link”. Khi đó Android:

  1. Lấy danh sách các host trong intent-filter đủ điều kiện (xem mục dưới).
  2. Với mỗi host, tải file https://<host>/.well-known/assetlinks.json.
  3. Kiểm tra file có khai báo đúng package_name của app và fingerprint SHA-256 của chứng chỉ ký app đang cài hay không.
  4. Ghi lại kết quả cho từng host. Host xác minh thành công thì link thuộc host đó mở thẳng app.

Bước xác minh chỉ xét scheme và host. Phần path (pathPrefix, pathPattern…) dùng để quyết định link nào được app nhận, không tham gia vào việc xác minh. Ngoại lệ: từ Android 15, assetlinks.json có thể chứa luật path động (Dynamic App Links), khi đó path cũng được áp dụng theo file.

Hệ thống chỉ xét các intent-filter có đủ:

  • Action android.intent.action.VIEW
  • Category android.intent.category.BROWSABLE và android.intent.category.DEFAULT
  • Scheme http hoặc https

Thiếu BROWSABLE (link từ trình duyệt và ứng dụng khác sẽ không tới được app) hoặc thiếu DEFAULT là lỗi khá phổ biến khi viết tay intent-filter.

Tài liệu Android khuyên đặt autoVerify="true" cho mọi intent-filter muốn được xác minh, thay vì chỉ một filter.

Phiên bảnThời điểm xác minh
Android 14 trở xuốngChỉ khi cài đặt hoặc cập nhật app. Sửa assetlinks.json sau đó thì máy đã cài không biết, phải chờ bản cập nhật tiếp theo (hoặc cài lại)
Android 15 trở lênKhi cài/cập nhật, và định kỳ chạy lại trong nền. Thay đổi trên file có thể mất tới 7 ngày mới tới hết các máy

Xác minh chạy bất đồng bộ sau khi cài, thường cần chờ khoảng 20 giây. Bấm link ngay sau khi cài xong mà thấy mở trình duyệt chưa chắc đã là cấu hình sai.

Hệ quả thực tế: nếu assetlinks.json bị lỗi đúng lúc người dùng cập nhật app (server bảo trì, file bị xoá nhầm khi deploy website), các máy Android 14 trở xuống sẽ mang trạng thái xác minh thất bại cho tới lần cập nhật sau. File này nên được coi như một phần của hạ tầng app: có giám sát, không để đội web sửa tuỳ ý.

Khác biệt giữa Android 11 trở xuống và Android 12 trở lên

Phần tiêu đề “Khác biệt giữa Android 11 trở xuống và Android 12 trở lên”
<intent-filter android:autoVerify="true">
...
<data android:scheme="https" />
<data android:host="shop.example.com" />
<data android:host="m.example.com" />
</intent-filter>

Giả sử shop.example.com có file đúng, còn m.example.com không có file:

  • Android 11 trở xuống: xác minh thất bại cho cả app. Chỉ cần một host hỏng là không host nào được công nhận, kể cả các host ở intent-filter khác.
  • Android 12 trở lên: xác minh từng host riêng. shop.example.com vẫn mở app, chỉ m.example.com mở trình duyệt.

Khi app vẫn còn người dùng Android 11 trở xuống, nên chỉ khai báo những host mà bạn kiểm soát được file assetlinks.json.

  • Android 11 trở xuống: link khớp intent-filter nhưng chưa xác minh sẽ hiện hộp thoại chọn ứng dụng.
  • Android 12 trở lên: link https chưa xác minh mặc định mở trình duyệt. Người dùng vẫn có thể tự bật cho app trong Settings → Apps → MyShop → Open by default → Add link.

Các hành vi Android 12 ở trên áp dụng cho app có targetSdk từ 31 trở lên. App target thấp hơn vẫn dùng cơ chế cũ, trừ khi bật compat change (xem lệnh ở phần kiểm tra bên dưới). Hiện Google Play đã yêu cầu target SDK cao hơn 31 từ lâu nên với app trên Play thì gần như luôn là hành vi mới.

<data android:host="*.example.com" />

Khớp mọi subdomain như shop.example.com, m.example.com. Với wildcard, file assetlinks.json phải đặt ở domain gốc: https://example.com/.well-known/assetlinks.json.

Ngoài trường hợp wildcard, mỗi subdomain là một host riêng biệt: example.com, www.example.com và shop.example.com khai báo riêng thì mỗi cái cần một file riêng.

Các thẻ <data> trong cùng filter được gộp lại

Phần tiêu đề “Các thẻ <data> trong cùng filter được gộp lại”

Mọi thẻ <data> trong cùng một intent-filter được gộp thành tổ hợp của tất cả scheme, host, path. Ví dụ:

<intent-filter android:autoVerify="true">
...
<data android:scheme="https" android:host="shop.example.com" android:pathPrefix="/products/" />
<data android:scheme="https" android:host="blog.example.com" android:pathPrefix="/posts/" />
</intent-filter>

Trông như hai luật riêng, nhưng thực tế filter sẽ nhận cả shop.example.com/posts/... và blog.example.com/products/.... Muốn tổ hợp chính xác thì tách thành hai intent-filter.

Không trộn custom scheme vào filter cần xác minh

Phần tiêu đề “Không trộn custom scheme vào filter cần xác minh”
<!-- SAI: custom scheme trong filter có autoVerify -->
<intent-filter android:autoVerify="true">
...
<data android:scheme="https" />
<data android:scheme="myshop" />
<data android:host="shop.example.com" />
</intent-filter>

Tài liệu Android ghi rõ: không đưa scheme nào khác ngoài http/https vào filter cần xác minh, vì sẽ làm xác minh thất bại. Custom scheme (myshop://) đặt ở một intent-filter riêng, không có autoVerify.

Về http: mẫu trong tài liệu Android khai báo cả http và https trong filter để link http:// cũ cũng mở được app. Trong app nên xử lý link http như https.

Terminal window
# Xem trạng thái xác minh từng host
adb shell pm get-app-links com.example.myshop

Kết quả mẫu:

com.example.myshop:
ID: 01234567-89ab-cdef-0123-456789abcdef
Signatures: [***]
Domain verification state:
shop.example.com: verified
m.example.com: 1024

Ý nghĩa các trạng thái:

Trạng tháiÝ nghĩa
verifiedXác minh thành công. Chỉ trạng thái này mới là đạt
noneChưa có kết quả. Chờ thêm vài phút rồi yêu cầu xác minh lại
approvedĐược ép duyệt, thường bằng lệnh shell
deniedBị ép từ chối, thường bằng lệnh shell
migratedKết quả giữ lại từ cơ chế xác minh cũ
restoredĐược duyệt sau khi khôi phục dữ liệu người dùng
legacy_failureBị cơ chế xác minh cũ từ chối, không rõ lý do cụ thể
system_configuredĐược duyệt tự động bởi cấu hình của thiết bị
1024 trở lênMã lỗi riêng của trình xác minh trên thiết bị. Kiểm tra mạng, file trên server, rồi xác minh lại

Quy trình xác minh lại sau khi sửa assetlinks.json (Android 12+):

Terminal window
# 1. Đưa trạng thái link của app về ban đầu
adb shell pm set-app-links --package com.example.myshop 0 all
# 2. Yêu cầu xác minh lại (máy phải có Internet), chờ vài phút
adb shell pm verify-app-links --re-verify com.example.myshop
# 3. Xem kết quả
adb shell pm get-app-links com.example.myshop

Nếu app target dưới Android 12, cần bật cơ chế xác minh mới trước bước 1:

Terminal window
adb shell am compat enable 175408749 com.example.myshop

Xem lựa chọn của người dùng (các domain người dùng tự bật/tắt trong Settings):

Terminal window
adb shell pm get-app-links --user cur com.example.myshop

Cách chắc chắn nhất để thử lại từ đầu vẫn là gỡ app, cài lại, chờ 20 giây rồi bấm link.

Từ Android 12 (API 31), app có thể tự kiểm tra domain nào đã được xác minh bằng DomainVerificationManager:

val manager = context.getSystemService(DomainVerificationManager::class.java)
val userState = manager.getDomainVerificationUserState(context.packageName)
// Domain đã xác minh qua assetlinks.json
val verifiedDomains = userState?.hostToStateMap
?.filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED }
// Domain người dùng tự chọn cho app mở
val selectedDomains = userState?.hostToStateMap
?.filterValues { it == DomainVerificationUserState.DOMAIN_STATE_SELECTED }
// Domain chưa được duyệt
val unapprovedDomains = userState?.hostToStateMap
?.filterValues { it == DomainVerificationUserState.DOMAIN_STATE_NONE }

Nếu có domain chưa được duyệt, có thể mở màn hình Open by default để người dùng tự bật:

val intent = Intent(
Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
Uri.parse("package:${context.packageName}"),
)
context.startActivity(intent)

Nên giải thích cho người dùng vì sao trước khi đưa họ tới màn hình này. Ngoài ra, có thể gửi các số liệu này về analytics để phát hiện sớm khi xác minh trên máy người dùng thật bị lỗi hàng loạt.

Mỗi package name, mỗi chứng chỉ ký cần có trong assetlinks.json

Phần tiêu đề “Mỗi package name, mỗi chứng chỉ ký cần có trong assetlinks.json”

Xác minh so cả package_name lẫn fingerprint của chứng chỉ ký bản đang cài. Các trường hợp hay bị quên:

  • Flavor/build type đổi applicationId, ví dụ bản dev là com.example.myshop.dev: cần thêm một mục riêng cho package này trong assetlinks.json (thường là file của domain staging).
  • Bản debug ký bằng debug key, bản build local ký bằng upload key, bản tải từ Google Play ký bằng app signing key: ba fingerprint khác nhau. Mảng sha256_cert_fingerprints chấp nhận nhiều giá trị. Xem thêm bài Google Play App Signing.
Phần tiêu đề “Nhiều activity hoặc nhiều app cùng nhận một link”
  • Nhiều activity trong cùng app có intent-filter khớp cùng một App Link: không đảm bảo activity nào sẽ nhận. Nên chỉ để một activity (thường là activity chính) nhận link rồi tự điều hướng bên trong.
  • Hai app cùng xác minh được cùng host và path (ví dụ bản lite và bản đầy đủ): chỉ app cài gần nhất nhận link.

Với Flutter, intent-filter đặt trong android/app/src/main/AndroidManifest.xml, trong thẻ <activity> của MainActivity. Mọi điều trong bài này áp dụng y nguyên, vì xác minh là việc của hệ điều hành, không liên quan tới framework.