Vấn đề
Việc nâng cấp ứng dụng lên targetSdkVersion 34 (Android 14) thường đi kèm với một bất ngờ khó chịu. Ứng dụng của bạn có thể bị crash ngay khi cố gắng đăng ký một BroadcastReceiver một cách linh hoạt (dynamic). Điều này thường xảy ra khi khởi động ứng dụng hoặc khi điều hướng đến một tính năng cụ thể. Stack trace sẽ trông như sau:
FATAL EXCEPTION: main
Process: com.example.myapp, PID: 12345
java.lang.SecurityException: One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn't being registered exclusively for system broadcasts
Lý do bảo mật
Google đang thắt chặt bảo mật ứng dụng để ngăn chặn "broadcast hijacking" (chiếm quyền điều khiển broadcast). Trước đây, các receiver được đăng ký trong mã nguồn (không phải trong Manifest) thường không rõ ràng. Hệ thống không thực thi nghiêm ngặt việc các ứng dụng khác trên thiết bị có thể gửi broadcast đến chúng hay không.
Bắt đầu từ API 34, bạn phải chỉ định rõ ràng. Bạn phải khai báo receiver của mình là exported (có thể truy cập bởi các ứng dụng khác) hoặc not exported (riêng tư cho ứng dụng của bạn). Nếu bạn bỏ qua flag này và receiver của bạn lắng nghe bất kỳ sự kiện nào khác ngoài các sự kiện hệ thống cụ thể như ACTION_AIRPLANE_MODE_CHANGED, Android sẽ dừng tiến trình để bảo vệ dữ liệu của bạn.
Cách khắc phục từng bước
1. Xác định các lệnh gọi đăng ký
Tìm kiếm trong mã nguồn tất cả các phiên bản của registerReceiver(). Bạn đang tìm kiếm "cách cũ", thiếu tham số flag thứ ba:
// Lệnh này sẽ gây crash trên các thiết bị Android 14
registerReceiver(myReceiver, intentFilter);
2. Chọn và áp dụng Flag chính xác
Trong 90% trường hợp, receiver của bạn chỉ được sử dụng để giao tiếp nội bộ trong ứng dụng. Đối với những trường hợp này, hãy sử dụng RECEIVER_NOT_EXPORTED. Nó giúp giữ cho logic nội bộ của ứng dụng được ẩn khỏi các ứng dụng khác đã cài đặt.
Kotlin:
val filter = IntentFilter("com.example.UPDATE_UI")
val flags = Context.RECEIVER_NOT_EXPORTED
registerReceiver(myReceiver, filter, flags)
Java:
IntentFilter filter = new IntentFilter("com.example.UPDATE_UI");
int flags = Context.RECEIVER_NOT_EXPORTED;
registerReceiver(myReceiver, filter, flags);
3. Duy trì tính tương thích ngược
Các flag này đã được giới thiệu trong API 33. Nếu bạn sử dụng trực tiếp Context.RECEIVER_NOT_EXPORTED trên thiết bị Android 12, ứng dụng của bạn sẽ bị crash với lỗi NoSuchFieldError.
Giải pháp tối ưu nhất là sử dụng ContextCompat từ thư viện AndroidX. Nó sẽ xử lý việc kiểm tra phiên bản cho bạn, đảm bảo ứng dụng chạy mượt mà trên mọi thiết bị từ Android 5.0 đến 14.
// Cách tiếp cận hiện đại được khuyến nghị
ContextCompat.registerReceiver(
context,
myReceiver,
new IntentFilter("com.example.UPDATE_UI"),
ContextCompat.RECEIVER_NOT_EXPORTED
);
Các ngoại lệ đối với Broadcast hệ thống
Nếu bạn đang đăng ký cho một broadcast chỉ dành cho hệ thống—như Intent.ACTION_POWER_CONNECTED—thì flag này không bắt buộc một cách nghiêm ngặt. Tuy nhiên, đừng quá phụ thuộc vào ngoại lệ này. Việc thêm flag một cách rõ ràng giúp mã nguồn của bạn sẵn sàng cho tương lai và dễ đọc hơn cho các đồng nghiệp.
Danh sách kiểm tra xác minh
- **Triển khai lên API 34:** Kiểm tra trên thiết bị Android 14 vật lý hoặc trình giả lập.
- **Kích hoạt Receiver:** Đảm bảo dòng lệnh `registerReceiver` thực sự được thực thi.
- **Kiểm tra bằng ADB:** Nếu bạn đã sử dụng `RECEIVER_NOT_EXPORTED`, hãy thử kích hoạt nó từ terminal: **`adb shell am broadcast -a com.example.UPDATE_UI`. Receiver không** được kích hoạt nếu bảo mật đang hoạt động chính xác.
Mẹo xử lý sự cố
- **SDK bên thứ ba:** Nếu mã của bạn trông ổn nhưng lỗi vẫn còn, hãy kiểm tra các dependency. Các phiên bản cũ của SDK phân tích hoặc quảng cáo thường sử dụng các phương thức đăng ký lỗi thời. Hãy cập nhật chúng lên phiên bản mới nhất.
- **Mã cũ (Legacy):** Nếu bạn vẫn đang sử dụng `LocalBroadcastManager`, bạn sẽ không thấy lỗi này. Tuy nhiên, Google đã ngừng hỗ trợ (deprecated) nó từ nhiều năm trước. Đây là thời điểm tuyệt vời để chuyển đổi logic đó sang **Kotlin Flows** hoặc **LiveData**.

