Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Giải thích về Máy quét Bluetooth AOSP 16:Hướng dẫn kỹ thuật đầy đủ

Giải thích về Máy quét Bluetooth AOSP 16:Hướng dẫn kỹ thuật đầy đủ

À, Bluetooth. Công nghệ mà tất cả chúng ta đều thích và ghét. Giống như một người bạn luôn muốn kết nối nhưng sau đó... lại không.

Trong nhiều năm, các nhà phát triển Android đã bị cuốn vào một mối tình lãng mạn đầy kịch tính, thường là bi thảm với Bluetooth. Chúng tôi đã vật lộn với những điều kỳ quặc của nó, cầu xin nó cứ hoạt động và lặng lẽ rơi nước mắt trước những lần ngắt kết nối bí ẩn của nó.

Nhưng điều gì sẽ xảy ra nếu tôi nói với bạn rằng mọi thứ sắp trở nên tốt hơn? Điều gì sẽ xảy ra nếu tôi nói với bạn rằng với Android 16, vị thần Bluetooth cuối cùng đã mỉm cười với chúng ta thì sao? Đó không phải là một giấc mơ, bạn của tôi. Đó là Máy quét Bluetooth AOSP 16 và nó ra đời để mang đến niềm hy vọng mới cho tâm hồn nhà phát triển mệt mỏi của chúng tôi.

Trong cuốn sổ tay này, chúng ta sẽ bắt đầu một cuộc hành trình. Cuộc hành trình vào trung tâm các tính năng Bluetooth mới của AOSP 16. Chúng ta sẽ cười, chúng ta sẽ khóc (hy vọng lần này là vì vui vẻ) và chúng ta sẽ học cách sử dụng những sức mạnh mới này mãi mãi. Chúng ta sẽ khám phá sự kỳ diệu của việc quét thụ động, sự kịch tính của các lý do mất liên kết và sự tiện lợi tuyệt đối của việc nhận UUID dịch vụ mà không gặp phải những rắc rối thông thường.

Khi kết thúc câu chuyện hoành tráng này, bạn sẽ có thể:

  • Xây dựng một máy quét Bluetooth hiệu quả đến mức gần như có tác dụng tâm linh.

  • Gỡ lỗi các vấn đề kết nối như một thám tử dày dạn kinh nghiệm.

  • Gây ấn tượng với bạn bè và đồng nghiệp bằng khả năng làm chủ Bluetooth mới của bạn.

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

Trước khi đi sâu vào phần này, bạn nên có hiểu biết cơ bản về phát triển Android và Kotlin. Nếu bạn đã từng cố gắng làm cho hai thiết bị nói chuyện với nhau và cuối cùng lại muốn ném máy tính của mình ra ngoài cửa sổ thì bạn đã đủ tiêu chuẩn.

Vì vậy, hãy lấy đồ uống yêu thích của bạn, mặc áo choàng mã hóa và sẵn sàng thức tỉnh Bluetooth!

Mục lục

  1. Sơ lược về lịch sử Bluetooth trong Android

  2. Có gì mới trong AOSP 16:Ba chàng lính ngự lâm

  3. Tìm hiểu sâu #1:Quét thụ động

  4. Tìm hiểu về BluetoothLeScanner

  5. Thực hành:Xây dựng máy quét thụ động đầu tiên của bạn

  6. Tìm hiểu sâu #2:Lý do mất kết nối Bluetooth

  7. Tìm hiểu sâu #3:UUID dịch vụ từ quảng cáo

  8. Chủ đề nâng cao:Nâng cấp trò chơi quét của bạn

  9. Các trường hợp sử dụng trong thế giới thực:Nơi Bluetooth xuất hiện

  10. Kiểm tra phiên bản API:Cách không làm hỏng ứng dụng của bạn

  11. Kiểm tra và gỡ lỗi:Phần thú vị (Chưa ai nói)

  12. Hiệu suất và các phương pháp hay nhất:Cách trở thành công dân Bluetooth tốt

  13. Kết luận:Tương lai là thụ động (và điều đó không sao cả)

Lược sử Bluetooth (Hoặc:Cách chúng ta học cách ngừng lo lắng và yêu thích sóng vô tuyến)

Thời kỳ đen tối:Bluetooth cổ điển

Ban đầu có Bluetooth cổ điển. Nó tương đương với kỹ thuật số của một vị khách trong bữa tiệc ồn ào, náo nhiệt. Nó có thể mang nhiều dữ liệu (chẳng hạn như những giai điệu yêu thích của bạn phát ra loa), nhưng chắc chắn nó rất ngốn pin. Nó rất tốt để truyền phát âm thanh, nhưng để truyền dữ liệu nhỏ, không thường xuyên? Nó giống như việc dùng vòi cứu hỏa để tưới cây trong nhà. Quá mức cần thiết và nói thẳng ra là có chút lộn xộn.

Các nhà phát triển trong thời đại này đã dành nhiều ngày vật lộn với BluetoothAdapter, BluetoothDevice và BluetoothSocket đáng sợ. Đó là thời điểm vô cùng bất ổn, khi một kết nối đơn giản có thể mất vài giây, hoặc... giả sử bạn có thể đi pha một tách cà phê. Và hao pin? Người dùng của bạn sẽ chứng kiến mức năng lượng điện thoại của họ tụt dốc nhanh hơn cả quả bóng chì.

Thời kỳ Phục hưng:Nhập Bluetooth Năng lượng thấp (BLE)

Sau đó, với Android 4.3, một anh hùng mới đã xuất hiện:Bluetooth Low Energy hay BLE. Đây không phải là Bluetooth của bố bạn. BLE có kiểu dáng đẹp, hiệu quả và bí ẩn. Nó được thiết kế để truyền dữ liệu trong thời gian ngắn, tiêu tốn năng lượng như rượu hảo hạng thay vì nốc cạn.

BLE là đứa trẻ tuyệt vời trong khối. Nó giới thiệu cho chúng ta một thế giới khả năng hoàn toàn mới:máy đo nhịp tim, đồng hồ thông minh và một triệu lẻ một thiết bị IoT có thể chạy trong nhiều tháng chỉ bằng một cục pin dạng đồng xu. Đó là một yếu tố thay đổi cuộc chơi.

Nhưng sức mạnh to lớn đi kèm với... sự phức tạp lớn lao. Chúng tôi phải học một ngôn ngữ hoàn toàn mới về GATT, GAP, dịch vụ và đặc điểm. Nó giống như việc chuyển từ việc viết những kịch bản đơn giản sang sáng tác một vở opera hoàn chỉnh. Tiềm năng rất lớn nhưng con đường học tập lại rất dốc.

Đứa trẻ có vấn đề:Đang quét

Và sau đó là quá trình quét. Hành động tìm kiếm những thiết bị mới, tiêu thụ nhiều năng lượng này. Trong những ngày đầu của BLE, việc quét vẫn còn hơi xa lạ. Đó là một quá trình năng động và ồn ào. Điện thoại của bạn sẽ hét vào khoảng không, "Có ai ở ngoài đó không?", rồi lắng nghe câu trả lời. Cách này có hiệu quả nhưng vẫn gây hao pin đáng kể, đặc biệt nếu ứng dụng của bạn cần quét trong thời gian dài.

Đó là tình thế tiến thoái lưỡng nan kinh điển của nhà phát triển:bạn cần tìm thiết bị nhưng bạn không muốn trở thành nguyên nhân khiến điện thoại của người dùng hết pin vào giờ ăn trưa. Trong nhiều năm, chúng tôi đã đi trên sợi dây này, cân bằng nhu cầu khám phá với lời cầu xin tuyệt vọng về thời lượng pin.

Đây là thế giới mà AOSP 16 được sinh ra. Một thế giới đang kêu gọi một cách tốt hơn để quét. Một thế giới sẵn sàng cho một anh hùng. Và anh hùng đó, các bạn của tôi, đang quét thụ động. Nhưng sẽ nói thêm về điều đó sau...

Có gì mới trong AOSP 16? (Spoiler:Nó thực sự rất tuyệt)

Được rồi, hãy đi đến những điều tốt đẹp. Nhóm Android đã cung cấp cho chúng ta những đồ chơi mới sáng bóng nào trong AOSP 16? Hóa ra, khá nhiều! Nhưng trước khi chúng ta mở quà, hãy nói về lịch giao hàng mới, bởi vì bây giờ thậm chí điều đó cũng hơi khác một chút.

Câu chuyện về hai lần phát hành

Trong một tình tiết gây sốc, Android đã quyết định tặng chúng ta hai bản phát hành API chính vào năm 2025. Đầu tiên, chúng ta có sự kiện chính, Android 16 (tên mã là "Baklava", vì ai lại không thích một chiếc bánh ngọt ngon?), diễn ra vào Quý 2. Đây là bản phát hành truyền thống, mang tính đột phá của bạn với tất cả những thay đổi về hành vi mà bạn đã biết và yêu thích (hoặc sợ hãi).

Nhưng sau đó, trong Q4, chúng ta nhận được màn thứ hai bất ngờ:một bản phát hành nhỏ, đây là lúc các sản phẩm Bluetooth mới của chúng tôi xuất hiện hoành tráng. Bản phát hành này hoàn toàn tập trung vào các tính năng và API mới mà không có những thay đổi đáng sợ, có thể phá vỡ ứng dụng. Nó giống như nhận được một món tráng miệng miễn phí sau khi bạn đã thanh toán hóa đơn.

Ba người lính ngự lâm của Bluetooth

Vậy bản phát hành Q4 này đã mang lại điều gì cho bữa tiệc Bluetooth? Tôi rất vui vì bạn đã hỏi. Nó mang đến ba anh hùng mới, sẵn sàng cứu chúng ta khỏi tai họa Bluetooth. Tôi gọi họ là... Ba chàng lính ngự lâm.

Tính năng

Ý chính

Tại sao bạn nên quan tâm

Quét thụ động

Khả năng nghe các thiết bị Bluetooth mà không cần hét vào mặt chúng.

Giờ đây, ứng dụng của bạn có thể trở thành một ninja tiết kiệm pin và im lặng.

Lý do mất trái phiếu

Cuối cùng là phần giải thích lý do tại sao kết nối Bluetooth của bạn bị hỏng.

Bạn có thể ngừng chơi trò đoán mò và thực sự gỡ lỗi các sự cố kết nối.

UUID dịch vụ từ quảng cáo

Lấy số liệu thống kê quan trọng của thiết bị trực tiếp từ quảng cáo của thiết bị đó.

Nó giống như hẹn hò tốc độ cho các thiết bị Bluetooth. Kết nối nhanh hơn, hiệu quả hơn.

Đây không chỉ là những điều chỉnh nhỏ, thưa các bạn. Đây là những cải tiến về chất lượng cuộc sống về cơ bản sẽ thay đổi cách chúng ta xây dựng và gỡ lỗi các ứng dụng hỗ trợ Bluetooth. Cứ như thể nhóm Android thực sự đã lắng nghe tiếng kêu cứu chung của chúng tôi. (Tôi biết, tôi cũng bị sốc.)

Trong một số phần tiếp theo, chúng ta sẽ tìm hiểu kỹ hơn về từng tính năng mới này. Chúng ta sẽ đi sâu vào mã, khám phá các trường hợp sử dụng và tìm hiểu cách khai thác sức mạnh của chúng. Vì vậy, hãy sẵn sàng gặp người lính ngự lâm đầu tiên của chúng ta:loại người mạnh mẽ, im lặng được gọi là Quét thụ động.

Tìm hiểu sâu #1:Quét thụ động

Hãy tưởng tượng bạn đang ở trong một thư viện. Bạn đang tìm kiếm một người bạn, nhưng bạn không biết họ ở đâu. Bạn có hai lựa chọn:

  • Đang quét: Bạn đứng giữa thư viện và hét lên, "Này, STEVE! BẠN CÓ Ở ĐÂY KHÔNG?" Điều này có hiệu quả nhưng cũng ồn ào, gây rối và sẽ khiến bạn bị thủ thư đuổi ra ngoài (người, theo cách tương tự này, là pin của người dùng của bạn).

  • Quét thụ động: Bạn lặng lẽ đi dạo quanh thư viện, lắng nghe tiếng cười khò khè đặc trưng của bạn mình. Bạn không nói một lời nào Bạn chỉ cần lắng nghe. Tính năng này mang tính lén lút, hiệu quả và sẽ không tiêu hao pin mạng xã hội (hoặc thực tế) của bạn.

Trong nhiều năm, tính năng quét Bluetooth của Android đã là chủ đề gây tranh cãi trong thư viện. Nhưng với AOSP 16, cuối cùng chúng ta cũng có thể trở thành người biết lắng nghe trong im lặng. Đây chính là sự kỳ diệu của việc quét thụ động.

Chủ động và Bị động:Cuộc đối đầu về mặt kỹ thuật

Trong thế giới BLE, các thiết bị gửi đi những gói thông tin nhỏ gọi là "quảng cáo". Đó là cách họ nói:"Này, tôi ở đây và đây là việc tôi làm!"

  • Đang quét: Khi điện thoại của bạn thực hiện quá trình quét đang hoạt động, điện thoại sẽ nghe thấy một quảng cáo rồi gửi lại SCAN_REQ (Yêu cầu quét). Về cơ bản nó có nghĩa là "Hãy kể cho tôi biết thêm!" Sau đó, thiết bị ngoại vi sẽ trả lời bằng SCAN_RSP (Phản hồi quét), chứa thông tin bổ sung.

  • Quét thụ động: Với chức năng quét thụ động, điện thoại của bạn sẽ nghe thấy quảng cáo... và thế là xong. Nó không gửi lại bất cứ điều gì. Nó chỉ lưu ý đến quảng cáo ban đầu và tiếp tục. Đó là cuộc trò chuyện một chiều.

Tại sao lại thụ động? Sức mạnh của sự im lặng

Vì vậy, tại sao điều này là một vấn đề lớn như vậy? Hai từ:tiêu thụ điện năng. Mỗi khi sóng vô tuyến điện thoại của bạn phải truyền đi thứ gì đó (chẳng hạn như SCAN_REQ), nó sẽ sử dụng năng lượng. Nếu ứng dụng của bạn liên tục quét tìm thiết bị thì những lần truyền nhỏ đó sẽ tăng lên và pin của người dùng sẽ phải trả giá.

Bằng cách chuyển sang chế độ quét thụ động, bạn đang yêu cầu radio chỉ lắng nghe. Không nói chuyện, chỉ lắng nghe. Điều này giúp giảm đáng kể mức năng lượng cần thiết để quét, khiến nó trở thành giải pháp hoàn hảo cho các ứng dụng cần giám sát các thiết bị ở gần trong thời gian dài.

Quy tắc:Cách trở thành chuyên gia Bluetooth

Vì vậy, làm cách nào để chúng tôi triển khai chế độ tàng hình mới này? Nó đơn giản một cách đáng ngạc nhiên. Tất cả đều phụ thuộc vào Cài đặt quét mà bạn sử dụng khi bắt đầu quét.

Trước đây, bạn có thể đã làm điều gì đó như thế này:

val settings = ScanSettings.Builder()
 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
 .build()

Bây giờ, với AOSP 16, chúng ta có một tùy chọn mới. Để bật tính năng quét thụ động, bạn chỉ cần đặt loại quét:

// This is the magic line!
.setScanMode(ScanSettings.SCAN_TYPE_PASSIVE)

Đợi đã, điều đó không thể đúng được. Tài liệu cho biết SCAN_TYPE_PASSIVE là loại quét, không phải chế độ quét. Và bạn đã đúng! Tôi xin lỗi, tôi hơi phấn khích quá. Cách chính xác để thực hiện việc này là đặt chế độ quét thành thụ động. Hãy thử lại lần nữa.

val settings = ScanSettings.Builder()
 // The actual magic line!
 .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) // This is the closest to passive
 .build()

Đợi đã, điều đó cũng không hoàn toàn đúng. Có vẻ như tôi đã vượt quá giới hạn rồi. Hãy cùng tham khảo các cuộn giấy chính thức... À, đây rồi! ScanSettings.Builder có một phương thức mới trong Android 16 QPR2. Đó không phải là setScanMode, đó là một cài đặt hoàn toàn mới.

Hãy giải quyết vấn đề này một lần và mãi mãi. Đây là cách chính xác để bật tính năng quét thụ động:

// Available in Android 16 QPR2 and later
val settings = ScanSettings.Builder()
 // This is the REAL magic line, I promise!
 .setScanType(ScanSettings.SCAN_TYPE_PASSIVE) 
 .build()

Và bạn có nó. Chỉ với một dòng đó, bạn đã biến ứng dụng của mình từ một khách du lịch ồn ào, ngốn pin thành một ninja Bluetooth im lặng, hiệu quả. Pin của người dùng sẽ cảm ơn bạn.

Tất nhiên, có một sự đánh đổi. Vì bạn không gửi SCAN_REQ nên bạn sẽ không nhận được dữ liệu bổ sung từ SCAN_RSP. Nhưng đối với nhiều trường hợp sử dụng, quảng cáo ban đầu là tất cả những gì bạn cần. Và việc tiết kiệm điện năng là điều đáng giá hơn thế.

Bây giờ chúng ta đã nắm vững nghệ thuật quét im lặng, hãy chuyển sang phần tiếp theo của câu đố:tìm hiểu chính BluetoothLeScanner.

Tìm hiểu về BluetoothLeScanner (Ngôi sao trong chương trình của chúng tôi)

Trước khi có thể thực sự nắm vững nghệ thuật quét Bluetooth, trước tiên chúng ta phải hiểu vũ khí chính của mình:BluetoothLeScanner. Hãy coi nó như Máy đo PKE từ Ghostbusters. Đó là công cụ chúng tôi sử dụng để phát hiện năng lượng vô hình (trong trường hợp của chúng tôi là quảng cáo BLE) trôi nổi xung quanh chúng tôi. Nhưng thực tế thì thiết bị săn ma này hoạt động như thế nào?

Kiến trúc:Một cái nhìn đằng sau bức màn

Ở mức độ cao, quá trình này khá đơn giản. Ứng dụng của bạn, sống thoải mái trong thế giới nhỏ bé của riêng mình, quyết định muốn tìm một số thiết bị BLE. Nó lấy một phiên bản của BluetoothLeScanner và nói:"Này, hãy đi tìm nội dung."

Dưới mui xe, rất nhiều điều đang xảy ra. BluetoothLeScanner giao tiếp với ngăn xếp Bluetooth của Android (tên mã là "Fluoride", nghe có vẻ như điều mà nha sĩ của bạn sẽ rất tự hào). Sau đó, ngăn xếp sẽ giao tiếp với bộ điều khiển Bluetooth của thiết bị, phần cứng thực tế thực hiện việc gửi và nhận sóng vô tuyến. Đó là một trường hợp điển hình của việc "nó phức tạp hơn vẻ ngoài của nó".

Súp bảng chữ cái:GATT, GAP và những người bạn

Khi bạn dấn thân vào thế giới của BLE, bạn sẽ nhanh chóng gặp cả đống từ viết tắt. Không hoảng loạn! Chúng không đáng sợ như vẻ ngoài của chúng. Hai điều quan trọng nhất cần hiểu là GAP và GATT.

  • GAP (Hồ sơ truy cập chung): Đây là tất cả về cách các thiết bị khám phá và kết nối với nhau. Hãy coi GAP như người bảo vệ ở hộp đêm. Nó quyết định ai sẽ nói chuyện với ai. Nó quản lý quảng cáo (thiết bị hét lên "Tôi ở đây!") và quét (ứng dụng của bạn lắng nghe những tiếng hét đó). BluetoothLeScanner của chúng tôi là nhân tố chính trong GAP-verse.

  • GATT (Hồ sơ thuộc tính chung): Khi hai thiết bị được kết nối, GATT sẽ tiếp quản. Nó xác định cách họ trao đổi dữ liệu. Hãy coi GATT như cuộc trò chuyện thực tế diễn ra bên trong hộp đêm. Đó là tất cả về Dịch vụ, Đặc điểm và Mô tả. Một thiết bị có thể có "Dịch vụ nhịp tim" chứa "Đặc điểm đo nhịp tim". Ứng dụng của bạn đọc hoặc ghi vào những đặc điểm này để lấy dữ liệu cần thiết.

Với mục đích quét, chúng ta chủ yếu đang sống trong thế giới GAP. Chúng tôi là những người đứng bên ngoài câu lạc bộ, lắng nghe những quảng cáo thú vị.

Vòng đời quét:Một vở kịch kịch gồm ba màn

Vòng đời của quá trình quét Bluetooth là một vở kịch đơn giản nhưng tao nhã.

  • Màn I: Sự chuẩn bị. Ứng dụng của bạn quyết định đã đến lúc quét. Nó lấy BluetoothLeScanner, tạo một bộ ScanFilters (để chỉ tìm các thiết bị cụ thể) và ScanSettings (để xác định cách quét, như chế độ thụ động mới của chúng tôi) và xác định ScanCallback.

  • Màn II: Quét. Ứng dụng của bạn gọi startScan(). Đài Bluetooth hoạt động trở lại, lắng nghe các quảng cáo phù hợp với bộ lọc của bạn. Khi tìm thấy, nó sẽ báo cáo lại cho ứng dụng của bạn thông qua phương thức onScanResult() trong ScanCallback.

  • Màn III: Sự kết thúc. Khi ứng dụng của bạn đã có đủ (hoặc quan trọng hơn là khi bạn tìm thấy thứ mình đang tìm kiếm), nó sẽ gọi stopScan(). Đài tắt nguồn và tất cả lại im lặng. Điều quan trọng là luôn dừng quá trình quét của bạn khi bạn hoàn tất. Quét giả mạo là nguyên nhân số một khiến người dùng phàn nàn "pin của tôi hết sau một giờ".

Và tóm lại đó là BluetoothLeScanner. Đó là cửa ngõ của chúng tôi đến với thế giới khám phá BLE. Nó mạnh mẽ, phức tạp nhưng khi chúng tôi đang tìm hiểu, nó ngày càng thông minh hơn và hiệu quả hơn với mỗi bản phát hành Android mới. Bây giờ chúng ta đã biết công cụ của mình, hãy bắt tay vào xây dựng máy quét thụ động đầu tiên của chúng ta!

Thực hành:Xây dựng máy quét thụ động đầu tiên của bạn

Lý thuyết thì hay, nhưng thành thật mà nói, chúng tôi là nhà phát triển. Chúng ta học bằng cách thực hiện (hoặc bằng cách sao chép và dán từ Stack Overflow). Đã đến lúc xắn tay áo, khởi động Android Studio và xây dựng thứ gì đó. Chúng tôi sẽ tạo một ứng dụng đơn giản sử dụng khả năng quét thụ động mới phát hiện của chúng tôi để tìm các thiết bị BLE lân cận.

Bước 1:Thẩm tra cấp phép

Trước khi viết một dòng Kotlin, chúng ta phải xoa dịu các vị thần cấp phép của Android. Đây là một nghi lễ thiêng liêng và thường gây khó chịu. Đối với chức năng quét Bluetooth, các quy tắc đã thay đổi một chút trong những năm qua.

Đầu tiên, hãy mở AndroidManifest.xml của bạn và thêm vào như sau:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- For Android 12 (API 31) and above -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- For older versions, you needed location permissions -->
<!-- You might still need this if you support older devices -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Nhìn vào các quyền mà chúng tôi đã khai báo ở trên, bạn có thể thấy sự phát triển của mô hình quyền Bluetooth của Android đang diễn ra theo thời gian thực.

Hai quyền đầu tiên, BLUETOOTHBLUETOOTH_ADMIN , là người bảo vệ cũ. Chúng đã xuất hiện từ những ngày đầu của Android và cung cấp chức năng Bluetooth cơ bản cũng như khả năng khám phá thiết bị. Khi đó chúng ta có BLUETOOTH_SCAN , được giới thiệu trong Android 12 (API 31) và thể hiện sự thay đổi lớn trong cách Google nghĩ về quyền riêng tư.

Vâng, bạn đang thấy điều đó đúng. Ngày xưa (trước Android 12), Google đã quyết định rằng việc tìm thiết bị Bluetooth về cơ bản giống như biết vị trí chính xác của người dùng. Điều này khá hợp lý:xét cho cùng, nếu bạn có thể thấy đèn hiệu Bluetooth nào ở gần đó, bạn có thể xác định vị trí của mình theo tam giác. Nhưng cũng hơi rùng rợn khi hỏi địa điểm chỉ để tìm một cặp tai nghe. Điều này dẫn đến tình huống khó xử khi người dùng thấy một ứng dụng quét Bluetooth đơn giản hỏi vị trí chính xác của họ và nghi ngờ là điều dễ hiểu.

Rất may, với Android 12, họ đã giới thiệu BLUETOOTH_SCAN sự cho phép, điều đó hợp lý hơn nhiều. Quyền này cuối cùng cho phép các ứng dụng quét tìm thiết bị Bluetooth mà không cần yêu cầu quyền truy cập vị trí, điều này có ý nghĩa hơn nhiều từ góc độ người dùng. Bạn vẫn cần yêu cầu quyền này trong thời gian chạy nhưng ít nhất bạn không phải giải thích cho người dùng lý do tại sao ứng dụng tìm tiện ích đơn giản của bạn muốn biết nơi họ sống.

Tuy nhiên, hãy chú ý đến hai quyền cuối cùng để truy cập vị trí. Đó là những tàn tích của hệ thống cũ. Nếu đang xây dựng một ứng dụng cần hỗ trợ các thiết bị cũ chạy Android 11 trở xuống, bạn cần giữ các quyền vị trí này trong tệp kê khai của mình để có khả năng tương thích ngược. Trên các thiết bị hiện đại, BLUETOOTH_SCAN chỉ có sự cho phép mới thực hiện được công việc.

Bước 2:Mã thức tỉnh

Được rồi, chúng ta hãy đến phần thú vị. Dưới đây là thông tin chi tiết về cách triển khai trình quét thụ động trong Hoạt động hoặc Đoạn của bạn.

Lấy máy quét

Trước tiên, chúng ta cần lấy một phiên bản của BluetoothLeScanner.

private val bluetoothAdapter: BluetoothAdapter? by lazy {
 val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
 bluetoothManager.adapter
}
private val bleScanner: BluetoothLeScanner? by lazy {
 bluetoothAdapter?.bluetoothLeScanner
}

Hãy phân tích những gì đang xảy ra trong đoạn mã trên. Chúng tôi đang sử dụng lazy của Kotlin ủy quyền, đây là một cách nói hoa mỹ để nói "đừng tạo đối tượng này cho đến khi tôi thực sự cần nó." Đây là một phương pháp hay vì việc lấy bộ điều hợp Bluetooth liên quan đến các cuộc gọi hệ thống và sẽ chẳng ích gì khi thực hiện công việc đó nếu chúng ta chưa bao giờ thực sự sử dụng nó.

Đầu tiên, chúng ta lấy BluetoothManager từ các dịch vụ của hệ thống. Hãy nghĩ đến BluetoothManager là người gác cổng cho mọi thứ Bluetooth trên thiết bị của bạn. Từ người quản lý này, chúng tôi nhận được BluetoothAdapter , đại diện cho phần cứng Bluetooth vật lý của thiết bị của bạn. Lưu ý rằng chúng tôi đang khai báo nó là null (BluetoothAdapter? ) bởi vì dù bạn có tin hay không thì không phải mọi thiết bị Android đều có Bluetooth. Một số máy tính bảng hoặc thiết bị ít người biết đến có thể không có phần cứng, vì vậy chúng ta cần chuẩn bị cho khả năng đó.

Sau khi có bộ chuyển đổi, chúng tôi có thể yêu cầu nó cung cấp BluetoothLeScanner . Đây là đối tượng thực tế mà chúng tôi sẽ sử dụng để thực hiện quá trình quét của mình. Một lần nữa, chúng ta đang sử dụng toán tử cuộc gọi an toàn (?. ) vì nếu bộ chuyển đổi không có giá trị (không có phần cứng Bluetooth), chúng tôi chắc chắn không thể lấy được máy quét từ nó. Chương trình phòng thủ này có vẻ hoang tưởng, nhưng nó là thứ giúp phân biệt các ứng dụng gặp sự cố một cách bí ẩn với các ứng dụng xử lý khéo léo các trường hợp nguy hiểm.

Xác định lệnh gọi lại

Đây là nơi phép thuật xảy ra. ScanCallback là một đối tượng sẽ lắng nghe kết quả quét. Chúng ta cần ghi đè hai phương thức:onScanResult và onScanFailed.

private val scanCallback = object : ScanCallback() {
 override fun onScanResult(callbackType: Int, result: ScanResult) {
 // We found a device! 
 // The 'result' object contains the device, RSSI, and advertisement data.
 Log.d("BleScanner", "Found device: ${result.device.address}, RSSI: ${result.rssi}")
 }
 override fun onScanFailed(errorCode: Int) {
 // This is the universe's way of telling you to take a break.
 // Or that something went horribly wrong.
 Log.e("BleScanner", "Scan failed with error code: $errorCode")
 }
}

ScanCallback chúng tôi đã xác định ở trên là đôi tai của ứng dụng trong thế giới Bluetooth. Khi máy quét tìm thấy một thiết bị, nó không chỉ lưu trữ thông tin ở đâu đó mà còn chủ động gọi lại ứng dụng của bạn thông qua đối tượng gọi lại này. Đây là chương trình hướng sự kiện cổ điển và là cách Android giữ cho ứng dụng của bạn phản hồi nhanh mà không chặn luồng chính.

onScanResult phương thức này được gọi mỗi khi máy quét phát hiện ra thiết bị phù hợp với bộ lọc của bạn (hoặc bất kỳ thiết bị nào nếu bạn không sử dụng bộ lọc). result tham số là một kho tàng thông tin. Nó chứa BluetoothDevice đối tượng (có tên và địa chỉ MAC của thiết bị), giá trị RSSI (Chỉ báo cường độ tín hiệu đã nhận – về cơ bản mức độ gần của thiết bị, với số cao hơn có nghĩa là gần hơn) và dữ liệu quảng cáo thô mà thiết bị đang phát sóng.

Trong ví dụ đơn giản ở trên, chúng tôi chỉ ghi địa chỉ MAC và RSSI, nhưng trong ứng dụng thực, bạn có thể muốn cập nhật giao diện người dùng, thêm thiết bị vào danh sách hoặc kích hoạt kết nối.

callbackType tham số cho bạn biết tại sao cuộc gọi lại này đã được kích hoạt. Có thể là CALLBACK_TYPE_ALL_MATCHES (mặc định, nghĩa là "đây là mọi thiết bị chúng tôi tìm thấy"), CALLBACK_TYPE_FIRST_MATCH (lần đầu tiên chúng tôi nhìn thấy thiết bị này) hoặc CALLBACK_TYPE_MATCH_LOST (chúng tôi đã không nhìn thấy thiết bị này một thời gian rồi nên có lẽ nó đã rời đi). Chúng ta sẽ tìm hiểu sâu hơn về các loại này trong phần nâng cao.

Sau đó là onScanFailed , phương thức mà tất cả chúng ta đều hy vọng sẽ không bao giờ được gọi nhưng chúng ta thực sự cần phải xử lý. Điều này được kích hoạt khi có sự cố nghiêm trọng xảy ra trong quá trình quét. Có thể bộ điều hợp Bluetooth đã bị tắt trong quá trình quét, có thể ứng dụng của bạn không có quyền phù hợp hoặc có thể bộ điều khiển Bluetooth vừa trải qua một ngày tồi tệ. errorCode sẽ cung cấp cho bạn gợi ý về điều gì đã xảy ra và bạn phải luôn ghi lại lỗi này và xử lý nó một cách khéo léo – có thể bằng cách hiển thị thông báo cho người dùng hoặc cố gắng khởi động lại quá trình quét sau khi bị trì hoãn.

Định cấu hình quét

Bây giờ, chúng tôi tạo ScanSettings. Đây là nơi chúng tôi nói với Android rằng chúng tôi muốn trở thành một ninja thụ động và tiết kiệm pin.

val scanSettings = ScanSettings.Builder()
 .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // Let's be nice to the battery
 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
 .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
 .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) // Report each ad once
 .setReportDelay(0L) // Report immediately
 // And here's the star of the show!
 .setScanType(ScanSettings.SCAN_TYPE_PASSIVE)
 .build()

ScanSettings đối tượng chúng tôi đang xây dựng ở trên giống như một hướng dẫn sử dụng chi tiết cho máy quét Bluetooth. Mỗi lệnh gọi phương thức sẽ tinh chỉnh chính xác cách hoạt động của quá trình quét và việc thực hiện đúng các cài đặt này là sự khác biệt giữa ứng dụng thân thiện với pin và ứng dụng bị gỡ cài đặt trong vòng vài giờ.

Chúng ta hãy đi qua từng cài đặt. Đầu tiên, setScanMode(SCAN_MODE_LOW_POWER) yêu cầu máy quét sử dụng chế độ quét năng lượng thấp, có nghĩa là nó sẽ quét theo khoảng thời gian thay vì liên tục. Điều này hoàn hảo cho hầu hết các trường hợp sử dụng khi bạn không cần kết quả tức thì và muốn duy trì tuổi thọ pin. Máy quét sẽ thức dậy, quét một chút, ngủ và lặp lại. Bluetooth tương đương với việc ngủ trưa.

Tiếp theo, setCallbackType(CALLBACK_TYPE_ALL_MATCHES) có nghĩa là chúng tôi muốn được thông báo mỗi khi máy quét tìm thấy thiết bị phù hợp. Đây là hành vi mặc định và là hành vi bạn sẽ sử dụng thường xuyên nhất. Như chúng tôi đã đề cập trước đó, bạn cũng có thể sử dụng CALLBACK_TYPE_FIRST_MATCH hoặc CALLBACK_TYPE_MATCH_LOST để phát hiện sự hiện diện phức tạp hơn.

setMatchMode(MATCH_MODE_AGGRESSIVE) cài đặt kiểm soát mức độ phần cứng sẽ cố gắng khớp các thiết bị với bộ lọc của bạn mạnh mẽ đến mức nào. MATCH_MODE_AGGRESSIVE có nghĩa là "báo cáo trùng khớp nhanh chóng, ngay cả khi bạn không chắc chắn 100%," trong khi MATCH_MODE_STICKY có nghĩa là "đợi cho đến khi bạn thực sự chắc chắn trước khi báo cáo." Chế độ linh hoạt mang lại cho bạn kết quả nhanh hơn nhưng đôi khi có thể cho kết quả dương tính giả.

Sau đó chúng ta có setNumOfMatches(MATCH_NUM_ONE_ADVERTISEMENT) , yêu cầu máy quét báo cáo một thiết bị sau khi chỉ nhìn thấy một quảng cáo từ thiết bị đó. Phương án thay thế là MATCH_NUM_FEW_ADVERTISEMENT , chờ nhiều quảng cáo trước khi báo cáo. Việc sử dụng một quảng cáo giúp bạn khám phá nhanh hơn, đồng thời chờ đợi một vài quảng cáo sẽ giảm kết quả dương tính giả từ các thiết bị vừa đi ngang qua.

setReportDelay(0L) thiết lập là rất quan trọng. Độ trễ 0 có nghĩa là "báo cáo kết quả ngay lập tức." Nếu bạn đặt giá trị này thành, chẳng hạn như 5000 mili giây, máy quét sẽ tổng hợp các kết quả và gửi chúng sau mỗi 5 giây. Tính năng quét theo đợt rất hữu ích cho việc quét ở chế độ nền (như chúng tôi đã thảo luận trong phần nâng cao), nhưng đối với việc quét ở nền trước khi người dùng đang tích cực chờ đợi thì bạn cần phải báo cáo ngay lập tức.

Và cuối cùng, ngôi sao của chương trình của chúng tôi:setScanType(SCAN_TYPE_PASSIVE) . Đây là API mới từ Android 16 QPR2 giúp biến máy quét của chúng tôi thành một trình lắng nghe im lặng. Thay vì chủ động gửi yêu cầu quét tới mọi thiết bị mà nó nghe thấy, nó chỉ lắng nghe các quảng cáo trôi nổi trong không khí. Cài đặt duy nhất này có thể giảm đáng kể mức tiêu thụ pin của ứng dụng trong quá trình quét. Đó là tính năng mà chúng tôi đã chờ đợi và nó thật tuyệt vời.

Bắt đầu và dừng quét

Cuối cùng, chúng ta cần các chức năng để bắt đầu và dừng quá trình quét. Hãy nhớ:luôn dừng quá trình quét của bạn! Một bản quét bị lãng quên là một con quái vật ngốn pin.

private fun startBleScan() {
 // Don't forget to request permissions first!
 if (bleScanner != null) {
 // You can add ScanFilters here to search for specific devices
 val scanFilters: List<ScanFilter> = listOf() 
 bleScanner.startScan(scanFilters, scanSettings, scanCallback)
 Log.d("BleScanner", "Scan started.")
 } else {
 Log.e("BleScanner", "Bluetooth is not available.")
 }
}
private fun stopBleScan() {
 if (bleScanner != null) {
 bleScanner.stopScan(scanCallback)
 Log.d("BleScanner", "Scan stopped.")
 }
}

Hai chức năng trên là công tắc bật/tắt cho máy quét Bluetooth của bạn và chúng có vẻ đơn giản vì tầm quan trọng của chúng. Hãy cùng phân tích xem điều gì đang xảy ra trong mỗi phần.

Trong startBleScan() , trước tiên chúng tôi kiểm tra xem bleScanner không phải là rỗng. Đây là mạng lưới an toàn của chúng tôi:nếu thiết bị không có phần cứng Bluetooth hoặc nếu Bluetooth bị tắt, máy quét sẽ không có giá trị và chúng tôi không muốn gặp sự cố khi cố gắng gọi các phương thức trên một đối tượng rỗng. Nếu máy quét tồn tại, chúng tôi gọi startScan() với ba tham số:danh sách ScanFilter các đối tượng, ScanSettings được chế tạo cẩn thận của chúng tôi , và ScanCallback chúng tôi đã xác định trước đó.

scanFilters danh sách hiện trống trong ví dụ của chúng tôi, có nghĩa là "tìm tất cả thiết bị BLE". Trong ứng dụng thực tế, bạn thường thêm bộ lọc vào đây để thu hẹp phạm vi tìm kiếm của mình.

Ví dụ:nếu bạn đang xây dựng một ứng dụng chỉ hoạt động với máy đo nhịp tim, bạn sẽ tạo một bộ lọc chỉ phù hợp với các thiết bị quảng cáo UUID Dịch vụ nhịp tim. Điều này rất quan trọng đối với cả hiệu suất và thời lượng pin:tại sao phải đánh thức ứng dụng của bạn cho mọi bàn chải đánh răng Bluetooth ngẫu nhiên khi bạn chỉ quan tâm đến máy theo dõi thể dục?

startScan() phương pháp khởi động quá trình quét. Kể từ thời điểm này, đài Bluetooth sẽ chủ động (hoặc trong trường hợp của chúng tôi là thụ động) lắng nghe quảng cáo và scanCallback của bạn sẽ bắt đầu nhận được kết quả. Đây là một hoạt động không đồng bộ, nghĩa là mã của bạn không chặn ở đây để chờ kết quả – thay vào đó, nó tiếp tục thực thi và kết quả sẽ đến thông qua lệnh gọi lại bất cứ khi nào chúng có sẵn.

Bây giờ hãy nói về stopBleScan() , đây có thể là hàm quan trọng nhất mà bạn viết. Khi bạn gọi stopScan() với cuộc gọi lại, bạn đang nói với đài Bluetooth, "Được rồi, chúng ta đã xong việc ở đây, bạn có thể quay lại ngủ." Điều này ngay lập tức dừng quá trình quét và giải phóng tài nguyên.

Điều quan trọng cần hiểu là nếu bạn không gọi lệnh này, quá trình quét sẽ tiếp tục chạy vô thời hạn, tiêu hao pin của người dùng như ma cà rồng tại ngân hàng máu ăn thỏa thích. Đây là lý do tại sao chúng tôi nhấn mạnh nó rất nhiều:một stopScan() bị lãng quên cuộc gọi là một trong những nguyên nhân phổ biến nhất gây ra khiếu nại hao pin trong ứng dụng Bluetooth.

Lưu ý rằng chúng ta đang chuyển cùng một scanCallback phản đối stopScan() mà chúng tôi đã sử dụng trong startScan() . Đây là cách Android biết nên dừng quá trình quét nào - về mặt lý thuyết, bạn có thể có nhiều lần quét đang chạy với các lệnh gọi lại khác nhau (mặc dù đó hiếm khi là một ý tưởng hay). Luôn đảm bảo rằng bạn đang dừng quá trình quét mà bạn đã bắt đầu bằng cách sử dụng cùng một tham chiếu gọi lại.

Kết hợp tất cả lại với nhau

Đây là một ví dụ hoàn chỉnh mà bạn có thể đưa vào Hoạt động. Chỉ cần nhớ xử lý các quyền trong thời gian chạy!

// In your Activity class
class MainActivity : AppCompatActivity() {
 // ... (lazy properties for adapter and scanner from above)
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 // ... your UI setup ...
 // Example: Start scan on button click
 val startButton = findViewById<Button>(R.id.startButton)
 startButton.setOnClickListener {
 // You MUST request permissions before calling this!
 startBleScan()
 }
 // Example: Stop scan on another button click
 val stopButton = findViewById<Button>(R.id.stopButton)
 stopButton.setOnClickListener {
 stopBleScan()
 }
 }
 // ... (scanCallback, startBleScan, stopBleScan functions from above)
 override fun onPause() {
 super.onPause()
 // Always stop scanning when the activity is not visible.
 stopBleScan()
 }
}

Ví dụ hoàn chỉnh ở trên cho thấy tất cả các phần khớp với nhau như thế nào trong một Hoạt động thực. Đây là một máy quét Bluetooth tối thiểu nhưng đầy đủ chức năng mà bạn thực sự có thể chạy. Hãy nêu bật một số mẫu quan trọng mà chúng tôi đang sử dụng ở đây.

Trước tiên, hãy chú ý cách chúng tôi gắn vòng đời quét với hành động của người dùng thông qua các lần nhấp vào nút. Đây là mẫu phổ biến:người dùng bắt đầu và dừng quá trình quét một cách rõ ràng, cho phép họ kiểm soát thời điểm ứng dụng đang sử dụng Bluetooth. Đây vừa là trải nghiệm người dùng tốt vừa tiết kiệm pin vì quá trình quét chỉ chạy khi người dùng muốn.

Nhưng đây mới là phần thực sự quan trọng:onPause() ghi đè. Đây là một mạng lưới an toàn quan trọng. Khi Hoạt động của bạn chuyển sang chế độ nền (có thể người dùng đã nhấn nút trang chủ hoặc họ đã chuyển sang ứng dụng khác), onPause() được gọi và chúng tôi ngay lập tức dừng quá trình quét. Điều này rất cần thiết vì nếu người dùng không thể nhìn thấy ứng dụng của bạn, họ không cần kết quả quét và không có lý do gì khiến họ tiêu hao pin. Mẫu này đảm bảo rằng ngay cả khi người dùng quên nhấn nút "Dừng", quá trình quét sẽ không chạy mãi trong nền.

Có thể bạn sẽ thắc mắc "Còn onResume() thì sao? ? Chúng ta có nên khởi động lại quá trình quét khi người dùng quay lại không?" Đó là một quyết định thiết kế. Trong một số ứng dụng, bạn có thể muốn tự động bắt đầu lại quá trình quét ở onResume() . Ở những nơi khác, bạn có thể muốn người dùng nhấn lại "Bắt đầu". Nó phụ thuộc vào trường hợp sử dụng của bạn. Đối với ứng dụng tìm kiếm thiết bị mà người dùng đang tích cực tìm kiếm, việc tự động tiếp tục là điều hợp lý. Đối với ứng dụng giám sát chạy ẩn, bạn có thể muốn có quyền kiểm soát rõ ràng hơn.

Một điều quan trọng mà chúng tôi chưa trình bày trong ví dụ này là việc xử lý quyền trong thời gian chạy. Bạn có nhớ những quyền mà chúng tôi đã khai báo trong bảng kê khai không? Trên Android 6.0 trở lên, bạn không thể chỉ khai báo chúng mà bạn phải thực sự yêu cầu chúng từ người dùng khi chạy. Trước khi gọi startBleScan() , bạn nên kiểm tra xem mình có các quyền cần thiết hay không, nếu không, hãy yêu cầu chúng bằng cách sử dụng ActivityCompat.requestPermissions() . Nếu bạn cố gắng bắt đầu quét mà không có quyền thích hợp, quá trình quét sẽ không thành công (hoặc ồn ào, tùy thuộc vào phiên bản Android) và bạn sẽ phải gãi đầu tự hỏi tại sao không có gì hoạt động.

Và bạn có nó! Bạn vừa xây dựng máy quét Bluetooth thụ động AOSP 16 đầu tiên của mình. Nó gọn gàng, nhỏ gọn và cực kỳ tiết kiệm điện. Máy quét sẽ âm thầm lắng nghe các quảng cáo BLE, báo cáo chúng thông qua lệnh gọi lại của bạn và dừng nhẹ nhàng khi không cần thiết.

Bây giờ, chúng ta hãy chuyển sang chủ đề tiếp theo:phải làm gì khi gặp sự cố. Đã đến lúc nói về sự tan vỡ... tức là sự tan vỡ của mối quan hệ Bluetooth.

Tìm hiểu sâu #2:Lý do mất kết nối Bluetooth

À, liên kết Bluetooth. Đó là một điều đẹp đẽ, thiêng liêng. Đó là sự tương đương kỹ thuật số của việc trao đổi vòng tay tình bạn. Khi bạn kết nối điện thoại với tai nghe, bạn đang tạo mối quan hệ lâu dài và đáng tin cậy. Họ chia sẻ các khóa bí mật, ghi nhớ lẫn nhau và hứa sẽ tự động kết nối, giúp bạn tránh khỏi rắc rối khi ghép nối mỗi lần. Đó là một mối tình lãng mạn đẹp đẽ.

Cho đến khi nó không còn nữa.

Đột nhiên, một ngày, họ... quên nhau. Kết nối đã biến mất. Niềm tin bị phá vỡ. Và ứng dụng của bạn bị bỏ lại ở giữa, cố gắng đóng vai nhà trị liệu mà không biết chuyện gì đã xảy ra. Bạn đã bị ma ám. Và cho đến thời điểm hiện tại, Android vẫn chưa giúp được gì. Bạn sẽ nhận được thông báo rằng trạng thái liên kết hiện là BOND_NONE, nhưng chỉ có vậy thôi. Không có lời giải thích. Không đóng cửa. Chỉ là sự im lặng lạnh lùng của một kết nối thất bại.

Cuối cùng, cũng đã kết thúc!

Nhưng những người bạn của chúng tôi trong nhóm Android rõ ràng đã trải qua một số cuộc chia tay khó khăn, bởi vì trong AOSP 16, họ đã trao cho chúng tôi món quà kết thúc. Giới thiệu BluetoothDevice.EXTRA_BOND_LOSS_REASON. Đây là một tính năng bổ sung mới đi kèm với chương trình phát sóng ACTION_BOND_STATE_CHANGED và nó ở đây để cho bạn biết lý do tại sao trái phiếu bị mất. Giống như nhận được một tin nhắn chia tay thực sự giải thích chuyện gì đã xảy ra!

Bây giờ, khi mối liên kết bị phá vỡ, bạn có thể nhận được mã lý do cụ thể. Hãy coi chúng như những lời bào chữa chia tay cổ điển, nhưng đối với Bluetooth:

Mã lý do (Minh họa)

Ý nghĩa thực sự của nó

BOND_LOSS_REASON_BREDR_AUTH_FAILURE

Cho biết lý do mất trái phiếu là do lỗi xác thực BREDR.

BOND_LOSS_REASON_BREDR_INCOMING_PAIRING

Cho biết lý do mất trái phiếu là do lỗi ghép nối BREDR.

BOND_LOSS_REASON_LE_ENCRYPT_FAILURE

Cho biết lý do mất trái phiếu là do lỗi mã hóa LE.

BOND_LOSS_REASON_LE_INCOMING_PAIRING

Indicates that the reason for the bond loss is LE pairing failure.

The Code:Playing Detective

So, how do we get this juicy gossip? We need to set up a BroadcastReceiver to listen for bond state changes.

// Create a BroadcastReceiver to listen for bond state changes
private val bondStateReceiver = object : BroadcastReceiver() {
 override fun onReceive(context: Context, intent: Intent) {
 if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
 val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
 val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
 val previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR)
 // Check if we went from bonded to not bonded
 if (bondState == BluetoothDevice.BOND_NONE && previousBondState == BluetoothDevice.BOND_BONDED) {
 Log.d("BondBreakup", "We got dumped by ${device?.address}!")
 // Now, let's find out why...
 val reason = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_LOSS_REASON, -1)
 when (reason) {
 // Note: The actual constant values are in the Android SDK
 BluetoothDevice.BOND_LOSS_REASON_REMOTE_DEVICE_REMOVED -> {
 Log.d("BondBreakup", "Reason: The remote device removed the bond.")
 // You could show a message to the user: "Your headphones seem to have forgotten you. Please try pairing again."
 }
 // ... handle other reasons ...
 else -> {
 Log.d("BondBreakup", "Reason: It's complicated (Unknown reason code: $reason)")
 }
 }
 }
 }
 }
}
// In your Activity or Service, register the receiver
override fun onResume() {
 super.onResume()
 val filter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
 registerReceiver(bondStateReceiver, filter)
}
override fun onPause() {
 super.onPause()
 // Don't forget to unregister!
 unregisterReceiver(bondStateReceiver)
}

The code above implements a detective system for Bluetooth bond breakups, and it's more sophisticated than it might first appear. Let's walk through how this broadcast receiver pattern works and why it's so powerful.

First, we're creating a BroadcastReceiver , which is Android's way of letting your app listen for system-wide events. Think of it as subscribing to a notification service, whenever something interesting happens in the Android system (like a bond state change), the system broadcasts an "intent" to all registered listeners. Our receiver is one of those listeners.

In the onReceive() method, we first check if the incoming intent's action is ACTION_BOND_STATE_CHANGED . This is crucial because broadcast receivers can potentially receive many different types of intents, and we only care about bond state changes. Once we've confirmed this is the right type of event, we extract the relevant information from the intent using getParcelableExtra() and getIntExtra() .

The device object tells us which Bluetooth device this event is about. After all, you might be bonded to multiple devices (your headphones, your smartwatch, your car), and we need to know which one just broke up with us. The bondState tells us the current state (are we bonded, bonding, or not bonded?), and previousBondState tells us what the state was before this change occurred.

The key logic happens in our conditional check:if (bondState == BluetoothDevice.BOND_NONE && previousBondState == BluetoothDevice.BOND_BONDED) . This is checking for the specific transition from "bonded" to "not bonded," which is the digital equivalent of a breakup. We're not interested in the bonding process itself (going from none to bonding to bonded) – we only care about when an existing bond is lost.

Once we've detected a breakup, we extract the new EXTRA_BOND_LOSS_REASON from the intent. This is the star feature from AOSP 16 that finally gives us closure. The reason code tells us exactly why the bond was lost – was it the remote device that ended things? Did the user manually forget the device? Did authentication fail? Each reason code corresponds to a different scenario, and you can handle each one appropriately.

In the example above, we're using a when expression to handle different reason codes. For BOND_LOSS_REASON_BREDR_INCOMING_PAIRING, we know the other device initiated the breakup, so we can show a helpful message like "Your headphones seem to have forgotten you. Please try pairing again." For other reasons, you'd add more branches to handle them specifically.

Now, notice the lifecycle management at the bottom. We register our receiver in onResume() and unregister it in onPause() . This is critical:if you forget to unregister a broadcast receiver, it will continue to receive broadcasts even after your Activity is destroyed, which can cause memory leaks and crashes. The pattern of registering in onResume() and unregistering in onPause() ensures that we only listen for bond changes when our Activity is visible and active.

This is a huge step forward for debugging and for user experience. Instead of just telling the user "Connection failed," you can now give them actionable advice based on the specific reason the bond was lost. It's like being a helpful, informed relationship counselor instead of a confused bystander who can only shrug and say "I don't know what happened."

Now that we've dealt with the emotional baggage of breakups, let's move on to something a little more lighthearted:speed dating for Bluetooth devices.

Deep Dive #3:Service UUIDs from Advertisements

Let's talk about finding a compatible partner... for your app. In the world of BLE, not all devices are created equal. A heart rate monitor is very different from a smart lightbulb. So how does your app know if it's talking to the right kind of device? The answer is the Service UUID.

What in the World is a Service UUID?

A Service UUID (Universally Unique Identifier) is like a device's job title. It's a unique, 128-bit number that says, "I am a device that provides a Heart Rate Service" or "I am a device that provides a Battery Service." It's the single most important piece of information for determining what a device can do.

The Old Way:The Awkward First Date

Traditionally, finding out a device's services was a whole ordeal. It was like going on a full, three-course dinner date just to find out the other person's job. The process went something like this:

  1. Scan:Find the device.

  2. Connect:Establish a connection (a slow and power-hungry process).

  3. Discover Services:Ask the device, "So... what do you do for a living?" and wait for it to list all its services.

  4. Evaluate:Check if the list of services contains the one you're interested in.

  5. Disconnect (or stay connected):If it's not the right device, you have to break up (disconnect) and move on. What a waste of time and energy!

This is incredibly inefficient, especially if you're in a crowded room with dozens of BLE devices and you're only looking for one specific type.

The New Way:The Glorious Name Tag

Wouldn't it be great if everyone at a party just wore a name tag with their job title on it? That's exactly what AOSP 16 has given us with BluetoothDevice.EXTRA_UUID_LE. Many BLE devices are already polite enough to include their primary service UUID in their advertisement packets. It's their way of shouting, "I'M A HEART RATE MONITOR!" to the whole room.

Before AOSP 16, getting this information out of the advertisement packet was a messy, manual process of parsing the raw byte array of the scan record. It was doable, but it was the kind of code that you'd write once, pray it worked, and never touch again.

Now, Android does the dirty work for us! The system automatically parses the advertising data and, if it finds any service UUIDs, it conveniently hands them to you in the ScanResult.

The Code:Reading the Name Tag

This new feature makes our ScanCallback even more powerful. We can now check the device's job title the moment we discover it, without ever having to connect.

private val scanCallback = object : ScanCallback() {
 override fun onScanResult(callbackType: Int, result: ScanResult) {
 Log.d("BleSpeedDating", "Found device: ${result.device.address}")
 // Let's check their name tag!
 val serviceUuids = result.scanRecord?.serviceUuids
 if (serviceUuids.isNullOrEmpty()) {
 Log.d("BleSpeedDating", "This one is mysterious. No service UUIDs in the ad.")
 return
 }
 // Define the UUID we're looking for (e.g., the standard Heart Rate Service UUID)
 val heartRateServiceUuid = ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
 if (serviceUuids.contains(heartRateServiceUuid)) {
 Log.d("BleSpeedDating", "It's a match! This is a heart rate monitor. Let's connect!")
 // Now you can proceed to connect to result.device, knowing it's the right one.
 stopBleScan() // We found what we were looking for
 // connectToDevice(result.device)
 } else {
 Log.d("BleSpeedDating", "Not a match. Moving on.")
 }
 }
 // ... onScanFailed ...
}

The code above demonstrates the power of reading service UUIDs directly from advertisement data, and it's a game-changer for device discovery. Let's break down exactly what's happening and why this is such a significant improvement.

When we receive a scan result in our callback, the result object contains a scanRecord tài sản. This scan record is essentially the raw advertisement packet that the BLE device broadcast into the air.

Before AOSP 16, if you wanted to extract service UUIDs from this data, you'd have to manually parse the byte array, understand the BLE advertisement format, handle different data types, and pray you didn't make an off-by-one error. It was the kind of code that worked once and then you never touched it again out of fear.

Now, with the improvements in AOSP 16, Android does all that messy parsing for us. We can simply call result.scanRecord?.serviceUuids and get back a nice, clean list of ParcelUuid đồ vật. The safe call operator (?. ) is important here because not all devices include a scan record in their results, and we need to handle that gracefully.

After retrieving the service UUIDs, we check if the list is null or empty. Some devices don't include service UUIDs in their advertisements. They might be using a proprietary format, or they might just be poorly configured. If there are no UUIDs, we log a message and return early. There's no point in continuing if we can't identify what the device does.

Next, we define the UUID we're looking for. In this example, we're searching for heart rate monitors, so we use the standard Heart Rate Service UUID:0000180D-0000-1000-8000-00805F9B34FB . This is a UUID defined by the Bluetooth SIG (Special Interest Group), and any compliant heart rate monitor will advertise this UUID. You can find a complete list of standard service UUIDs in the Bluetooth specifications, or you can use custom UUIDs if you're building your own BLE peripherals.

The magic happens in the if (serviceUuids.contains(heartRateServiceUuid)) check. This is where we're doing our speed dating:we're checking the device's "name tag" to see if it matches what we're looking for.

If it does, we've found our match! We can immediately stop scanning (because why keep looking when we've found what we need?) and proceed to connect to the device. We know, with certainty, that this device is a heart rate monitor, so we won't waste time and battery connecting to random devices only to discover they're not what we need.

If the UUID doesn't match, we simply log "Not a match" and move on. The callback will be called again when the next device is found, and we'll repeat this process until we find our heart rate monitor or the user stops the scan.

This is a massive performance improvement over the old approach. Previously, you'd have to connect to every device you found, perform service discovery (which involves multiple round-trip communications with the device), check if it has the services you need, and then disconnect if it doesn't. Each connection attempt takes time, uses battery, and creates unnecessary radio traffic.

Now, you can filter and identify devices at lightning speed, all at the scanning stage. No more awkward first dates where you connect to a smart lightbulb thinking it might be a fitness tracker. Just efficient, targeted connections.

This is particularly useful for apps that need to find a specific type of sensor or peripheral in a sea of irrelevant devices. Imagine you're in a hospital with hundreds of BLE-enabled medical devices, or in a smart home with dozens of sensors and actuators. Being able to instantly identify the right device from its advertisement is the difference between a responsive, professional app and one that feels sluggish and unreliable.

We've now met all three of our Bluetooth musketeers:passive scanning for battery efficiency, bond loss reasons for better debugging, and service UUIDs from advertisements for faster device identification. But our journey isn't over. It's time to venture into the deep woods of advanced scanning techniques.

Advanced Topics:Filtering, Batching, and Other Sorcery

Alright, you've mastered the basics. You can scan passively, you can get closure on your connection breakups, and you can speed-date devices like a pro. You're no longer a Bluetooth padawan. It's time to become a Jedi Master.

Let's dive into the advanced arts of filtering, batching, and other optimization sorcery that will make your app a true battery-saving champion.

Hardware Filtering:Your Personal Assistant

Imagine you're a celebrity, and you've hired a personal assistant. You don't want to be bothered by every single person who wants an autograph. So, you give your assistant a list:"Only let me know if you see my agent or my mom." Your assistant then stands at the door and only bothers you when someone on the list shows up.

This is exactly what hardware filtering does. Instead of your app's code (the celebrity) being woken up for every single Bluetooth device the radio sees, you can offload the filtering logic to the Bluetooth controller itself (the personal assistant). This is a feature that's been around since Android 6.0, but it's more important than ever.

Why is this so great? Because your app's code can stay asleep. The main processor (the AP) doesn't have to wake up every time a random Bluetooth toothbrush advertises itself. The Bluetooth controller, which is much more power-efficient, handles the filtering. The AP only wakes up when the controller finds a device that matches your criteria.

The Code:Building Your VIP List

You implement this using ScanFilter. You can filter by a device's name, its MAC address, or, most usefully, by the Service UUID it's advertising.

// We only want to be bothered if we see a heart rate monitor.
val heartRateServiceUuid = ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
val filter = ScanFilter.Builder()
 .setServiceUuid(heartRateServiceUuid)
 .build()
val scanFilters: List<ScanFilter> = listOf(filter)
// Now, when you start your scan, pass in this list
bleScanner.startScan(scanFilters, scanSettings, scanCallback)

The code above shows how to create a hardware-level filter that dramatically improves both battery life and app performance. Let's dive deep into what's happening here and why this is such a powerful technique.

We start by defining the service UUID we're interested in – in this case, the standard Heart Rate Service UUID. This is the same UUID we used in the previous example, but now we're using it in a fundamentally different way. Instead of checking the UUID in our app's code after receiving scan results, we're telling the Bluetooth hardware itself to only report devices that match this UUID.

The ScanFilter.Builder() is our tool for constructing this filter. It's a builder pattern, which means we can chain multiple methods together to configure exactly what we're looking for. In this example, we're calling setServiceUuid(heartRateServiceUuid) , which tells the filter to only match devices that advertise this specific service.

But the builder has many other options you can use:

  • setDeviceName() – Match devices with a specific name (like "My Heart Monitor")

  • setDeviceAddress() – Match a specific device by its MAC address (useful if you've already paired with a device and want to find it again)

  • setManufacturerData() – Match devices based on manufacturer-specific data in their advertisements

  • setServiceData() – Match based on service data included in the advertisement

You can even combine multiple criteria in a single filter. For example, you could create a filter that matches devices with a specific service UUID and a specific manufacturer ID. The more specific your filter, the fewer false positives you'll get.

After building our filter, we create a list containing it. Why a list? Because you can have multiple filters, and a device will match if it satisfies any of the filters in the list. For instance, you might create one filter for heart rate monitors and another for blood pressure monitors, and your scan will report devices that match either one. This is an OR operation:the device doesn't need to match all filters, just one of them.

Finally, we pass this list of filters to startScan() along with our scan settings and callback. Đây là nơi phép thuật xảy ra. When you provide filters, Android doesn't just filter the results in your app's code. It pushes these filters down to the Bluetooth controller hardware itself. This means the filtering happens at the lowest level, before your app is even notified.

Here's why this is so powerful:without filters, every time the Bluetooth radio hears an advertisement from any device (your neighbor's smart toaster, someone's fitness tracker walking by, the Bluetooth speaker three rooms away), it has to wake up your app's process, deliver the scan result, and let your code decide if it cares about this device. Each of these wake-ups costs battery and processing time.

With hardware filters, the Bluetooth controller silently ignores all the devices that don't match your criteria. Your app stays asleep. The main processor stays asleep. Only when a heart rate monitor is detected does the hardware wake up your app and deliver the result. It's like having a bouncer at a club who only lets in people on the VIP list. Everyone else is turned away at the door, and you never even know they were there.

By using a ScanFilter , you're telling the hardware, "Don't wake me up unless you see a heart rate monitor." It's the ultimate power-saving move for background scanning. Combined with passive scanning and batch reporting, you can create a Bluetooth scanning system that runs for hours or even days with minimal battery impact. This is how professional-grade apps handle long-term device monitoring without destroying battery life.

Batch Scanning:The Daily Report

Let's go back to our celebrity analogy. Sometimes, you don't need to be interrupted the moment your mom shows up. You'd rather just get a report at the end of the day:"Today, your mom stopped by twice, and your agent called once." This is batch scanning.

Instead of delivering scan results to your app in real-time, the Bluetooth controller can collect them and deliver them in a big batch. This is another incredible power-saving feature. Your app can sleep for long periods, then wake up, process a whole bunch of results at once, and go back to sleep.

You enable this with the setReportDelay() method in your ScanSettings.

val scanSettings = ScanSettings.Builder()
 // ... other settings ...
 // Deliver results every 5 seconds (5000 milliseconds)
 .setReportDelay(5000)
 .build()

When you use a report delay, your onScanResult callback will be replaced by onBatchScanResults, which gives you a List.

private val scanCallback = object : ScanCallback() {
 override fun onBatchScanResults(results: List<ScanResult>) {
 Log.d("BatchScanner", "Here's your daily report! Found ${results.size} devices.")
 for (result in results) {
 // Process each result
 }
 }
 // ... onScanFailed ...
}

The batch scanning mechanism shown above is one of the most underutilized power-saving features in Android Bluetooth, and understanding how it works can transform your app's battery profile. Let's break down exactly what's happening under the hood and when you should use this technique.

When you set a report delay of 5000 milliseconds (5 seconds) in the code above, you're fundamentally changing how the scanning pipeline works. Instead of the Bluetooth controller immediately waking up your app every time it sees a device, it acts like a diligent assistant taking notes. For those 5 seconds, the controller silently collects every scan result it encounters, storing them in its own internal buffer. Your app remains completely asleep during this time – no CPU cycles wasted, no battery drained by context switches or process wake-ups.

After the 5-second delay expires, the controller delivers all the accumulated results in one batch to your onBatchScanResults() callback. This is where the power savings come from:instead of waking up your app 50 times if 50 devices were detected, it wakes up once and hands you all 50 results at the same time. Your app can then efficiently process this batch – maybe updating a UI list, logging the data, or checking for specific devices – and then go back to sleep until the next batch arrives.

The results parameter in onBatchScanResults() is a List<ScanResult> , and each ScanResult in the list represents a single advertisement that was heard during the batching period. It's important to note that if the same device advertises multiple times during the delay period, you might receive multiple results for that device in the batch. The list isn't automatically deduplicated – that's your job if you need it.

In the example above, we're simply logging the number of devices found and then iterating through each result. In a real application, you might want to do more sophisticated processing. For instance, you could build a map of devices keyed by MAC address to track how many times each device advertised, calculate average RSSI values to estimate distance, or filter the batch to only process devices that meet certain criteria.

Cảnh báo: Batch scanning is a powerful tool, but it's not for every situation. If you need to react to a device's presence immediately (for example, if you're building a "find my keys" app where the user is actively searching), a report delay is not your friend. The user doesn't want to wait 5 seconds to see results – they want instant feedback. In these cases, set setReportDelay(0) for immediate reporting.

But for long-term monitoring or data collection scenarios, batch scanning is a battery's best friend. Consider these use cases:

  • Background presence monitoring :Your app checks every minute to see if the user's smartwatch is still in range, but doesn't need second-by-second updates.

  • Environmental sensing :You're collecting data from temperature sensors throughout a building and only need to update your dashboard every 30 seconds.

  • Beacon analytics :You're tracking how many people pass by a retail location based on their phone's BLE advertisements, and you aggregate the data every 10 seconds.

The sweet spot for report delay depends on your use case. Too short (like 1 second), and you're not getting much benefit, you're still waking up frequently. Too long (like 60 seconds), and your app might feel unresponsive or miss time-sensitive events. For most background monitoring tasks, delays between 5 and 30 seconds work well.

One more thing to be aware of:batch scanning has limits. The Bluetooth controller has a finite buffer for storing scan results. If you set a very long delay and you're in an environment with hundreds of BLE devices, the buffer might fill up before the delay expires. When this happens, the oldest results get dropped. Android doesn't give you a warning when this occurs, so if you're missing data, consider reducing your report delay or using more aggressive filters to reduce the number of results being collected.

OnFound/OnLost:The Drama of Presence

Since Android 8.0, scanning has gotten even more dramatic. You can now ask the hardware to not only tell you when it finds a device, but also when it loses one. This is done using the CALLBACK_TYPE_FIRST_MATCH and CALLBACK_TYPE_MATCH_LOST flags in your ScanSettings.

val scanSettings = ScanSettings.Builder()
 .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH or ScanSettings.CALLBACK_TYPE_MATCH_LOST)
 .build()

Now, in your ScanCallback, the callbackType parameter in onScanResult will tell you what happened.

override fun onScanResult(callbackType: Int, result: ScanResult) {
 when (callbackType) {
 ScanSettings.CALLBACK_TYPE_FIRST_MATCH -> {
 Log.d("PresenceDetector", "Found them! ${result.device.address} has entered the building.")
 }
 ScanSettings.CALLBACK_TYPE_MATCH_LOST -> {
 Log.d("PresenceDetector", "They're gone! ${result.device.address} has left the building.")
 }
 }
}

The presence detection mechanism shown above represents a fundamental shift in how we think about Bluetooth scanning. Instead of treating scanning as a continuous stream of "here's what I see right now," we're now working with events:"this device appeared" and "this device disappeared." Let's dive deep into how this works and why it's so powerful.

When you set the callback type using the bitwise OR operator (or in Kotlin, | in Java), you're telling the Bluetooth hardware to track the presence state of devices over time. The code CALLBACK_TYPE_FIRST_MATCH or CALLBACK_TYPE_MATCH_LOST combines both flags, meaning you want to be notified both when a device first appears and when it disappears. You can use these flags individually if you only care about one type of event, but using both together gives you complete presence awareness.

Let's understand what "first match" and "match lost" actually mean. When the Bluetooth controller hears an advertisement from a device that matches your filters for the first time, it triggers a CALLBACK_TYPE_FIRST_MATCH event. This is different from CALLBACK_TYPE_ALL_MATCHES (the default), which would trigger every single time the device advertises. A device might advertise multiple times per second, so the difference is significant. With FIRST_MATCH , you get one notification when the device enters your scanning range, not a flood of notifications as it continues to advertise.

The CALLBACK_TYPE_MATCH_LOST event is even more interesting. The Bluetooth controller keeps track of when it last heard from each device. If a device stops advertising (because it moved out of range, was turned off, or its battery died), the controller notices the absence and triggers a MATCH_LOST event. This happens automatically:you don't have to manually track timestamps or implement timeout logic in your app. The hardware does it for you.

But how does the hardware know when a device is "lost"? It uses an internal timeout. If the controller hasn't heard from a device for a certain period (typically a few seconds, though the exact duration is implementation-dependent and not exposed to apps), it considers the device lost. This means there's a slight delay between when a device actually leaves range and when you get the MATCH_LOST callback, but this delay is usually acceptable for presence detection use cases.

In the code example above, we're using a when expression to handle the different callback types. When we receive a FIRST_MATCH , we know the device has just entered our scanning range, so we log "Found them!" This is perfect for triggering actions like unlocking a door when your phone comes near, or starting to sync data when your fitness tracker is detected.

When we receive a MATCH_LOST , we know the device has left our scanning range or stopped advertising, so we log "They're gone!" This is ideal for triggering cleanup actions like locking the door when your phone leaves, or stopping a data sync when your tracker disconnects.

This is incredibly useful for presence detection scenarios. Is your smart lock in range? Is your fitness tracker still connected? Is the user's phone nearby? Now you can know, with hardware-level certainty, and you can react to changes in presence without constantly polling or maintaining complex state machines in your app code.

Here's a practical example of how you might use this in a smart home app:

private val presenceCallback = object : ScanCallback() {
 override fun onScanResult(callbackType: Int, result: ScanResult) {
 when (callbackType) {
 ScanSettings.CALLBACK_TYPE_FIRST_MATCH -> {
 // User's phone detected - they're home!
 Log.d("SmartHome", "Welcome home! Unlocking door and turning on lights.")
 unlockFrontDoor()
 turnOnLights()
 adjustThermostat(COMFORTABLE_TEMP)
 }
 ScanSettings.CALLBACK_TYPE_MATCH_LOST -> {
 // User's phone is gone - they left!
 Log.d("SmartHome", "Goodbye! Locking door and entering away mode.")
 lockFrontDoor()
 turnOffLights()
 adjustThermostat(ENERGY_SAVING_TEMP)
 armSecuritySystem()
 }
 }
 }
 override fun onScanFailed(errorCode: Int) {
 Log.e("SmartHome", "Presence detection failed: $errorCode")
 }
}

One important consideration:FIRST_MATCH and MATCH_LOST are mutually exclusive with CALLBACK_TYPE_ALL_MATCHES . If you combine them with ALL_MATCHES , the behavior becomes undefined and varies by device. Stick to either ALL_MATCHES for continuous reporting, or FIRST_MATCH /MATCH_LOST for presence detection – don't try to use both at once.

Also, be aware that presence detection works best when combined with hardware filtering. If you're scanning for all devices without filters, the controller has to track the presence state of every single BLE device in range, which can overwhelm its internal tracking tables. Always use ScanFilter to narrow down which devices you care about when using presence detection.

By combining these advanced techniques – hardware filtering, batch scanning, and presence detection – you can build incredibly sophisticated and power-efficient Bluetooth applications. You're not just a developer anymore. You're a Bluetooth wizard, wielding the power to create apps that are aware of their surroundings, responsive to changes, and respectful of battery life.

Now, let's see where we can apply these magical powers in the real world.

Real-World Use Cases:Where the Bluetooth Hits the Road

Okay, we've learned a ton of cool new tricks. We're basically Bluetooth black belts at this point. But what's the use of all this power if we don't use it for good (or at least for a cool app)? Let's explore some real-world scenarios where the new features in AOSP 16 can turn a good app into a great one.

1. The "Find My Everything" App

Tất cả chúng tôi đã ở đó. You're late for work, and your keys have decided to play a game of hide-and-seek in another dimension. This is the classic use case for a BLE tracker.

  • The Old Way: Your app would be constantly doing active scans, draining your battery while you frantically search. It would connect to every tracker in your house just to see if it's the right one.

  • The AOSP 16 Way: Your app runs a passive scan in the background with a hardware filter for your tracker's specific Service UUID. The battery impact is minimal. When you open the app to find your keys, it already knows they're in the house because it's been listening silently. You hit the "Find" button, the app connects, and your keys start screaming from inside the couch cushions. And if the connection fails? Bond loss reason tells you if the tracker's battery died, so you're not looking for a dead device.

2. The Smart Supermarket

Imagine an app that gives you coupons for products as you walk past them in the store. This is the dream of proximity marketing, a dream that has been historically thwarted by, you guessed it, battery drain.

  • The Old Way: The app would need to constantly scan for beacons, turning the user's phone into a hot potato and a dead battery by the time they reach the checkout line.

  • The AOSP 16 Way: The supermarket places BLE beacons in each aisle. Your app uses a passive, batched scan. It wakes up every minute or so, gets a list of all the beacons it has seen, and then goes back to sleep. When it sees you've been loitering in the cookie aisle for five minutes (it knows, it always knows), it uses the Service UUID from the advertisement to identify the "Cookie Aisle Beacon" and sends you a coupon for Oreos. It's targeted, it's efficient, and it doesn't kill your battery before you can pay.

3. The Overly-Attached Smart Home

Your smart home should be, well, smart. It should know when you're home and when you've left. It should lock the door behind you and turn on the lights when you arrive.

  • The Old Way: You'd have to rely on GPS (a notorious battery hog) or Wi-Fi connections, which can be unreliable. BLE was an option, but constant scanning was a problem.

  • The AOSP 16 Way: Your phone is the key. Your smart hub (acting as a central device) runs a continuous, low-power passive scan. When it sees your phone's BLE advertisement, it knows you're home. But what if you just walk by the house? This is where the OnFound/OnLost feature comes in. The hub can be configured to only trigger the "Welcome Home" sequence after it has seen your device consistently for a minute (OnFound), and to trigger the "Goodbye" sequence only after it hasn't seen you for five minutes (OnLost). It's a smarter, more reliable presence detection system that finally makes the smart home feel... smart.

4. The Corporate Asset Tracker

In a large hospital or warehouse, keeping track of expensive, mobile equipment (like IV pumps or forklifts) is a huge challenge. BLE tags are the solution.

  • The Old Way: Employees would have to walk around with a tablet, doing active scans to take inventory. It's slow, manual, and inefficient.

  • The AOSP 16 Way: A network of fixed BLE gateways is installed throughout the building. Each gateway is a simple device (like a Raspberry Pi) running a continuous passive scan. They collect all the advertisement data from the asset tags and send it to a central server. The server can now see, in real-time, that IV Pump #34 is in Room 201, and Forklift #3 is currently in the loading bay. No manual scanning required. It's a low-cost, low-power, real-time location system, all thanks to the efficiency of passive scanning.

These are just a few examples. From fitness trackers to industrial sensors, the new Bluetooth features in AOSP 16 open up a world of possibilities for building apps that are not only powerful but also polite to your user's battery. Now, let's talk about how to make sure our shiny new app works on all devices, not just the new ones.

API Version Checking:How to Not Crash Your App

So, you've built a beautiful, battery-sipping app using all the new hotness from AOSP 16's Q4 release. You're ready to ship it, become a millionaire, and retire to a private island. But then, a bug report comes in. Your app is crashing on a brand new Android 16 device. What gives?!

Welcome, my friend, to the wonderful world of API version checking. With Android's new release schedule, this has become more important (and slightly more complicated) than ever.

The Problem:A Tale of Two Android 16s

As we discussed, 2025 gave us two Android 16 releases:

  • The Q2 Release: The main "Baklava" release. Let's call this API level 36.0.

  • The Q4 Release: The minor, feature-drop release. This is where our new Bluetooth toys live. Let's call this API level 36.1.

Our new passive scanning API, setScanType(), only exists on 36.1 and later. If you try to call it on a device that's running the initial Q2 release (36.0), your app will crash with a NoSuchMethodError. It's the digital equivalent of asking for a menu item that was only added last night. The chef (your app) just gets confused and has a meltdown.

The Old Guard:SDK_INT

For years, our trusty friend for checking API levels has been Build.VERSION.SDK_INT. It's simple and effective.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 // Use an API from Android 12 (S) or higher
}

But SDK_INT only knows about major releases. For both Android 16 Q2 and Q4, SDK_INT will just report 36. It has no idea about the minor version. It's like asking someone their age, and they just say "thirties." Not very specific.

The New Hotness:SDK_INT_FULL

To solve this, the Android team has given us a new, more precise tool:Build.VERSION.SDK_INT_FULL . This constant knows about both the major and minor version numbers. And to go with it, we have a new set of version codes:Build.VERSION_CODES_FULL .

So, to safely call our new passive scanning API, we need to do a more specific check:

// Let's build our ScanSettings
val scanSettingsBuilder = ScanSettings.Builder()
 .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
// Now, let's check if we can go passive
if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.BAKLAVA_1) {
 Log.d("ApiCheck", "This device is cool. Going passive.")
 // This is the new API from the Q4 release (36.1)
 scanSettingsBuilder.setScanType(ScanSettings.SCAN_TYPE_PASSIVE)
} else {
 Log.d("ApiCheck", "This device is old school. Sticking to active scanning.")
 // Fallback for devices that don't have the new API
 // We don't need to do anything here, as active is the default
}
val scanSettings = scanSettingsBuilder.build()

Graceful Degradation:The Art of Falling with Style

This brings us to a crucial concept:graceful degradation. It means your app should still work on older devices, even if it can't use the latest and greatest features. It should fall back gracefully.

In our example above, if the setScanType method isn't available, we just... don't call it. The app will default to a normal, active scan. It won't be as battery-efficient, but it will still work. The user on the older device gets a functional app, and the user on the newer device gets a more optimized experience. Everybody wins.

Here's a table to help you remember when to use which check:

If you're using an API from...

Use this check...

A major Android release (for example, Android 16 Q2)

if (SDK_INT>=VERSION_CODES.BAKLAVA)

A minor, feature-drop release (for example, Android 16 Q4)

if (SDK_INT_FULL>=VERSION_CODES_FULL.BAKLAVA_1)

Mastering this new API checking is non-negotiable. It's the key to writing modern Android apps that are both innovative and stable. Now that we know how to build a robust app, let's talk about how to fix it when it inevitably breaks.

Testing and Debugging:The Fun Part (Said No One Ever)

There are two universal truths in software development:

  • It works on my machine, and

  • It will break in the most spectacular way possible during a live demo.

Bluetooth development, in particular, seems to delight in this second truth. It's a fickle, invisible force that seems to have a personal vendetta against developers.

So, how do we fight back? With a solid testing and debugging strategy. It's not glamorous, but it's the only way to stay sane.

The Emulator:A Land of Make-Believe

Android Studio's emulator is a fantastic tool. It's fast, it's convenient, and it can simulate all sorts of devices. And for Bluetooth? It can... sort of help. The emulator does have virtual Bluetooth support. You can enable it, and your app will think it has a Bluetooth adapter. It's great for testing your UI and making sure your app doesn't crash when it tries to get the BluetoothLeScanner.

But here's the catch:it's not real. The emulator can't actually interact with the radio waves in your room. You can't use it to find your real-life BLE headphones. For that, you need to venture into the real world.

The Real World:Where the Bugs Live

There is no substitute for testing on real, physical devices. Every phone manufacturer has its own special flavor of Bluetooth stack, its own quirky antenna design, and its own unique way of making your life difficult. A scan that works perfectly on a Google Pixel might fail miserably on another brand. The only way to know is to test.

Your testing arsenal should include:

  • A variety of phones: Different brands, different Android versions. The more, the better.

  • A variety of BLE peripherals: Don't just test with one type of device. Get a few different beacons, sensors, or wearables. You'll be amazed at how differently they behave.

Common Errors:The Usual Suspects

When your scan inevitably fails, it will give you an error code. Here are a few of the most common culprits:

Error Code

The Problem

How to Fix It

SCAN_FAILED_ALREADY_STARTED

You tried to start a scan that was already running.

You got too excited. Make sure you're not calling startScan() multiple times without calling stopScan() in between.

SCAN_FAILED_APPLICATION_REGISTRATION_FAILED

Something is fundamentally wrong with your app's setup.

This is a vague and unhelpful error. It usually means you have a problem with your permissions or the system is just having a bad day. Try restarting Bluetooth.

SCAN_FAILED_INTERNAL_ERROR

The Bluetooth stack had a panic attack.

This is the classic "it's not you, it's me" error. It's an internal issue with the device's Bluetooth controller. There's not much you can do except try again later.

SCAN_FAILED_FEATURE_UNSUPPORTED

You tried to use a feature the hardware doesn't support.

You might be trying to use batch scanning on a device that doesn't support it. Use your API version checks!

Debugging Tools:Your Ghost-Hunting Kit

When things go wrong, you need the right tools to see what's happening in the invisible world of Bluetooth.

  • logcat: This is your best friend. Be generous with your log statements. Log when you start a scan, when you stop a scan, when you find a device, and when a scan fails. Create a filter for your app's tag so you can see the signal through the noise.

  • Android's Bluetooth HCI Snoop Log: This is the holy grail of Bluetooth debugging. It's a developer option that records every single Bluetooth packet that goes in or out of your device. It's incredibly detailed and can be overwhelming, but it's the ultimate source of truth. You can open the generated log file in a tool like Wireshark to see the raw, unfiltered conversation between your phone and the BLE device. It's like having a wiretap on the radio waves.

  • nRF Connect for Mobile: This is a free app from Nordic Semiconductor, and it's an essential tool for any BLE developer. It lets you scan for devices, see their advertising data, connect to them, and explore their GATT services. If your app can't find a device, the first thing you should do is see if nRF Connect can. If it can't, the problem is likely with the peripheral, not your app.

Testing and debugging Bluetooth is a marathon, not a sprint. It requires patience, a methodical approach, and a healthy dose of self-deprecating humor. But with the right tools and techniques, you can tame the beast.

Now, let's talk about how to make sure our well-behaved app is also a good citizen when it comes to performance.

Performance and Best Practices:How to Be a Good Bluetooth Citizen

Writing code that works is one thing. Writing code that works well, is efficient, and doesn't make your users want to throw their phone against a wall is another thing entirely. When it comes to Bluetooth, being a good citizen is all about one thing:battery, battery, battery.

The Bluetooth radio is a powerful piece of hardware, but it's also a thirsty one. Every moment it's active, it's sipping power. Your job is to make sure it's only sipping when absolutely necessary. Here are the golden rules of being a good Bluetooth citizen.

1. Don't Scan If You Don't Have To

This sounds obvious, but it's the most common mistake. Before you even think about starting a scan, ask yourself:"Do I really need to do this right now?" If the user is not on the screen that needs scan results, don't scan. If the app is in the background, be extra critical. Background scanning is a huge drain on battery and is heavily restricted by Android for that very reason.

2. Stop Your Scan!

I'm going to say it again because it's that important:always stop your scan when you're done. A scan that's left running is like a leaky faucet for your battery. It will drain and drain until there's nothing left. The best practice is to tie your scan lifecycle to your UI lifecycle.

override fun onPause() {
 super.onPause()
 // The user can't see the screen, so they don't need the results.
 stopBleScan()
}
override fun onResume() {
 super.onResume()
 // The user is back on the screen, let's start scanning again.
 startBleScan()
}

If you find the device you're looking for, stop the scan immediately. There's no need to keep looking.

3. Choose the Right Scan Mode

ScanSettings gives you a few different modes. Choose wisely.

  • SCAN_MODE_LOW_POWER: This is your default, everyday mode. It scans in intervals, balancing discovery speed and battery life. Use this for most foreground scanning.

  • SCAN_MODE_BALANCED: A middle ground. It scans more frequently than low power mode.

  • SCAN_MODE_LOW_LATENCY: This is the "I need to find it NOW" mode. It scans continuously. This will find devices the fastest, but it will also drain your battery the fastest. Only use this for short, critical operations.

  • SCAN_MODE_OPPORTUNISTIC: This is the ultimate passive mode. Your app doesn't trigger a scan at all. It just gets results if another app happens to be scanning. It uses zero extra battery, but you have no guarantee of getting results. Use this for non-critical background updates.

And of course, if you're on AOSP 16 QPR2 or later, use setScanType(SCAN_TYPE_PASSIVE) whenever you don't need the scan response data. It's the new king of power efficiency.

4. Use Hardware Filtering and Batching

We covered this in the advanced section, but it's a best practice that's worth repeating. If you're looking for a specific device, use a ScanFilter. If you're doing a long-running scan, use setReportDelay() to batch your results. These two techniques offload the work to the power-efficient Bluetooth controller and let your app's code sleep, which is the number one way to save battery.

5. Be Mindful of Memory

Every ScanResult object that your app receives takes up memory. If you're in a crowded area with hundreds of BLE devices, and you're not using filters, your app can quickly get overwhelmed and run out of memory. This is another reason why filtering is so important. Only get the results you actually care about.

By following these rules, you can build a Bluetooth app that is not only powerful and feature-rich but also respectful of your user's device. You'll be a true Bluetooth sensei. Now, let's wrap things up and look to the future.

Conclusion:The Future is Passive (and That's Okay)

We've been on quite a journey, haven't we? We've traveled back in time to the dark ages of Classic Bluetooth, witnessed the renaissance of BLE, and emerged into the brave new world of AOSP 16. We've learned to be silent ninjas with passive scanning, played detective with bond loss reasons, and mastered the art of speed dating with service UUIDs from advertisements.

If there's one big takeaway from all of this, it's that the future of Bluetooth on Android is smarter, more efficient, and a whole lot less frustrating. The Android team is clearly listening to the pain points of developers and giving us the tools we need to build better, more battery-friendly apps. The introduction of passive scanning isn't just a new feature – it's a change in philosophy. It's an acknowledgment that sometimes, the best way to communicate is to just listen.

As developers, these new tools empower us to move beyond the simple "connect and stream" use cases. We can now build sophisticated, context-aware applications that are constantly aware of their surroundings without turning our users' phones into expensive paperweights. The dream of a truly smart, seamlessly connected world is a little bit closer, and it's going to be built on the back of these power-efficient technologies.

So, what's next? The world of Bluetooth is always evolving. We have Bluetooth 5.4 with Auracast, mesh networking, and even more precise location-finding on the horizon. The one thing we can be sure of is that the tools will continue to get better, and the challenges will continue to get more interesting.

For now, take a moment to appreciate the progress we've made. The next time you start a Bluetooth scan and it just works, take a moment to thank the hardworking engineers who made it possible. And the next time your app's battery graph is a beautiful, flat line instead of a terrifying ski slope, give a little nod to the power of passive scanning.

The Bluetooth beast may never be fully tamed, but with AOSP 16, we've been given a much stronger leash. Now go forth and build amazing things. And for the love of all that is holy, remember to stop your scan.

Học cách viết mã miễn phí. Chương trình giảng dạy mã nguồn mở của freeCodeCamp đã giúp hơn 40.000 người có được việc làm với tư cách là nhà phát triển. Bắt đầu