Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

อธิบายเครื่องสแกนบลูทูธ AOSP 16:คู่มือทางเทคนิคฉบับสมบูรณ์

อธิบายเครื่องสแกนบลูทูธ AOSP 16:คู่มือทางเทคนิคฉบับสมบูรณ์

เอ่อ บลูทูธ เทคโนโลยีที่เราทุกคนชอบที่จะเกลียด มันเหมือนกับเพื่อนคนหนึ่งที่มักจะเชื่อมต่ออยู่เสมอ แต่แล้ว... กลับไม่

เป็นเวลาหลายปีแล้วที่นักพัฒนา Android ติดอยู่กับความรักที่ดราม่าและมักจะน่าเศร้าด้วย Bluetooth เราต่อสู้กับนิสัยแปลกๆ ของมัน ขอร้องให้มันทำงาน และหลั่งน้ำตาเงียบๆ กับความเชื่อมโยงอันลึกลับที่ลดลง

แต่ถ้าฉันบอกคุณว่าสิ่งต่างๆ กำลังจะดีขึ้นล่ะ? จะเป็นอย่างไรถ้าฉันบอกคุณว่าในที่สุดเหล่าเทพแห่ง Bluetooth ก็ยิ้มให้กับเราด้วย Android 16 มันไม่ใช่ความฝันนะเพื่อน นี่คือเครื่องสแกนบลูทูธ AOSP 16 และพร้อมนำความหวังใหม่มาสู่จิตวิญญาณนักพัฒนาที่เหนื่อยล้าของเรา

ในคู่มือเล่มนี้ เรากำลังออกเดินทาง การเดินทางสู่ใจกลางคุณสมบัติ Bluetooth ใหม่ของ AOSP 16 เราจะหัวเราะ เราจะร้องไห้ (หวังว่าจะมีความสุขในครั้งนี้) และเราจะเรียนรู้วิธีใช้พลังใหม่เหล่านี้ให้ดี เราจะสำรวจความมหัศจรรย์ของการสแกนแบบพาสซีฟ เรื่องดราม่าของเหตุผลการสูญเสียพันธบัตร และความสะดวกในการรับ UUID ของบริการโดยไม่ต้องยุ่งยากตามปกติ

เมื่อสิ้นสุดตำนานมหากาพย์นี้ คุณจะสามารถ:

  • สร้างเครื่องสแกนบลูทูธที่มีประสิทธิภาพมาก ใช้งานได้จริง

  • แก้ไขปัญหาการเชื่อมต่อเหมือนนักสืบผู้ช่ำชอง

  • สร้างความประทับใจให้เพื่อนและเพื่อนร่วมงานของคุณด้วยความเชี่ยวชาญด้าน Bluetooth ที่เพิ่งค้นพบ

ข้อกำหนดเบื้องต้น:

ก่อนที่เราจะเจาะลึก เป็นความคิดที่ดีที่จะมีความเข้าใจพื้นฐานเกี่ยวกับการพัฒนา Android และ Kotlin หากคุณเคยพยายามทำให้อุปกรณ์สองเครื่องพูดคุยกัน และสุดท้ายก็อยากจะโยนคอมพิวเตอร์ของคุณออกไปนอกหน้าต่าง แสดงว่าคุณมีคุณสมบัติเกินควร

คว้าเครื่องดื่มแก้วโปรดของคุณ ใส่เสื้อคลุมสำหรับเขียนโค้ด แล้วเตรียมพร้อมสำหรับการตื่นตัวของ Bluetooth!

สารบัญ

  1. ประวัติโดยย่อของ Bluetooth ใน Android

  2. มีอะไรใหม่ใน AOSP 16:สามทหารเสือ

  3. เจาะลึก #1:การสแกนแบบพาสซีฟ

  4. ทำความเข้าใจกับ BluetoothLeScanner

  5. ลงมือปฏิบัติ:สร้างเครื่องสแกนแบบพาสซีฟเครื่องแรกของคุณ

  6. เจาะลึก #2:เหตุผลในการสูญเสียพันธบัตร Bluetooth

  7. เจาะลึก #3:บริการ UUID จากโฆษณา

  8. หัวข้อขั้นสูง:ยกระดับเกมการสแกนของคุณ

  9. กรณีการใช้งานจริง:จุดที่บลูทูธเข้าถึงท้องถนน

  10. การตรวจสอบเวอร์ชัน API:วิธีที่จะไม่ทำให้แอปของคุณเสียหาย

  11. การทดสอบและการดีบัก:ส่วนที่สนุก (ไม่มีใครเคยกล่าวไว้)

  12. ประสิทธิภาพและแนวทางปฏิบัติที่ดีที่สุด:จะเป็นพลเมือง Bluetooth ที่ดีได้อย่างไร

  13. บทสรุป:อนาคตเป็นแบบพาสซีฟ (และไม่เป็นไร)

ประวัติโดยย่อของ Bluetooth (หรือ:เราเรียนรู้ที่จะหยุดกังวลและรักคลื่นวิทยุได้อย่างไร)

ยุคมืด:บลูทูธคลาสสิก

ในตอนแรกมีบลูทูธแบบคลาสสิก มันเทียบเท่ากับแขกในงานปาร์ตี้ที่ส่งเสียงดังและอึกทึกทางดิจิทัล มันสามารถพกพาข้อมูลได้มากมาย (เช่นเพลงโปรดของคุณไปยังลำโพง) แต่แน่นอนว่าต้องใช้แบตเตอรี่มาก มันยอดเยี่ยมสำหรับการสตรีมเสียง แต่สำหรับการถ่ายโอนข้อมูลขนาดเล็กและไม่บ่อยนักล่ะ? มันเหมือนกับการใช้สายยางดับเพลิงรดน้ำต้นไม้ในบ้าน เกินกำลัง และจริงๆ แล้ว ยุ่งนิดหน่อย

นักพัฒนาในยุคนี้ใช้เวลาทั้งวันไปกับการต่อสู้กับ BluetoothAdapter, BluetoothDevice และ BluetoothSocket อันน่าสะพรึงกลัว มันเป็นช่วงเวลาแห่งความไม่แน่นอนอย่างมาก ซึ่งการเชื่อมต่อง่ายๆ อาจใช้เวลาไม่กี่วินาที หรือ... เอาล่ะ สมมติว่าคุณสามารถไปชงกาแฟสักแก้วได้ และแบตเตอรี่หมด? ผู้ใช้ของคุณจะเห็นว่าระดับพลังงานของโทรศัพท์ลดลงเร็วกว่าบอลลูนตะกั่ว

ยุคฟื้นฟูศิลปวิทยา:เข้าสู่ Bluetooth Low Energy (BLE)

จากนั้นด้วย Android 4.3 ฮีโร่ตัวใหม่ก็ปรากฏตัวขึ้น:Bluetooth Low Energy หรือ BLE นี่ไม่ใช่บลูทูธของพ่อคุณ BLE มีความทันสมัย ​​มีประสิทธิภาพ และลึกลับ ได้รับการออกแบบมาเพื่อการรับส่งข้อมูลในช่วงเวลาสั้นๆ โดยจิบพลังเหมือนไวน์ชั้นดีแทนที่จะดื่มจุใจ

BLE เป็นเด็กเท่ในตึกนี้ มันนำเราไปสู่โลกใหม่แห่งความเป็นไปได้:เครื่องวัดอัตราการเต้นของหัวใจ นาฬิกาอัจฉริยะ และอุปกรณ์ IoT หนึ่งล้านเครื่องที่สามารถใช้งานได้นานหลายเดือนด้วยแบตเตอรี่เซลล์แบบเหรียญเดียว มันเป็นตัวเปลี่ยนเกม

แต่ด้วยพลังอันยิ่งใหญ่ก็มา... ความซับซ้อนอันยิ่งใหญ่ เราต้องเรียนรู้ภาษาใหม่ของ GATT, GAP, บริการ และคุณลักษณะเฉพาะ มันเหมือนกับการเปลี่ยนจากการเขียนบทธรรมดาๆ ไปสู่การแต่งโอเปร่าที่เต็มอิ่ม มีศักยภาพมหาศาล แต่เส้นโค้งการเรียนรู้สูงชัน

เด็กที่มีปัญหา:การสแกน

แล้วก็มีการสแกน การค้นหาอุปกรณ์ใหม่ที่สิ้นเปลืองพลังงานเหล่านี้ ในช่วงแรก ๆ ของ BLE การสแกนยังค่อนข้างจะค่อนข้างไปทางตะวันตก มันเป็นกระบวนการที่กระฉับกระเฉงและมีเสียงดัง โทรศัพท์ของคุณจะตะโกนไปในความว่างเปล่าว่า "มีใครอยู่ข้างนอกบ้างไหม?" แล้วฟังคำตอบ วิธีนี้ได้ผล แต่ก็ยังเป็นการสิ้นเปลืองพลังงานอย่างมาก โดยเฉพาะอย่างยิ่งหากแอปของคุณจำเป็นต้องสแกนเป็นเวลานาน

มันเป็นปัญหาที่กลืนไม่เข้าคายไม่ออกของนักพัฒนาแบบคลาสสิก:คุณต้องค้นหาอุปกรณ์ แต่คุณไม่ต้องการเป็นสาเหตุที่ทำให้โทรศัพท์ของผู้ใช้ของคุณเสียก่อนเวลาอาหารกลางวัน เราเดินไต่เชือกนี้มานานหลายปี โดยรักษาสมดุลระหว่างความจำเป็นในการค้นพบกับคำร้องขออายุการใช้งานแบตเตอรี่

นี่คือโลกที่ AOSP 16 ถือกำเนิดขึ้นมา โลกที่เรียกร้องวิธีที่ดีกว่าในการสแกน โลกที่พร้อมสำหรับฮีโร่ และฮีโร่คนนั้น เพื่อนของฉัน กำลังสแกนแบบพาสซีฟ แต่เพิ่มเติมเกี่ยวกับเรื่องนั้นในอีกสักครู่...

มีอะไรใหม่ใน AOSP 16? (สปอยเลอร์:มันเจ๋งจริงๆ)

เอาล่ะ เรามาเข้าเรื่องกันดีกว่า ทีมงาน Android มอบของเล่นใหม่เอี่ยมอะไรให้เราใน AOSP 16 ปรากฎว่ามีไม่น้อย! แต่ก่อนที่เราจะแกะของขวัญ เรามาพูดถึงกำหนดการส่งมอบใหม่กันดีกว่า เพราะตอนนี้แม้จะแตกต่างออกไปเล็กน้อยก็ตาม

เรื่องราวของสองข่าว

ด้วยพล็อตเรื่องที่พลิกผันจนน่าตกใจ Android ตัดสินใจเปิดตัว API หลักสองรายการในปี 2025 อันดับแรก เรามีกิจกรรมหลักคือ Android 16 (ชื่อรหัสว่า "Baklava" เพราะใครล่ะจะไม่ชอบขนมอบดีๆ) ซึ่งมาถึงในไตรมาสที่ 2 นี่เป็นการเปิดตัวครั้งยิ่งใหญ่แบบดั้งเดิมของคุณพร้อมการเปลี่ยนแปลงพฤติกรรมทั้งหมดที่คุณรู้จักและชื่นชอบ (หรือความกลัว)

แต่แล้วในไตรมาสที่ 4 เราได้พบกับองก์ที่สองที่น่าประหลาดใจ นั่นคือการเปิดตัวเล็กน้อย ซึ่งเป็นจุดที่ผลิตภัณฑ์ Bluetooth ใหม่ของเราได้เข้ามาอย่างยิ่งใหญ่ ข่าวประชาสัมพันธ์ฉบับนี้เป็นเรื่องเกี่ยวกับฟีเจอร์และ API ใหม่โดยไม่มีการเปลี่ยนแปลงที่น่ากลัวและทำลายแอป เหมือนได้รับของหวานฟรีหลังจากที่คุณชำระเงินเรียบร้อยแล้ว

สามทหารเสือแห่งบลูทูธ

แล้วการเปิดตัวในไตรมาสที่ 4 นี้นำอะไรมาสู่ปาร์ตี้ Bluetooth? ฉันดีใจที่คุณถาม มีฮีโร่ใหม่สามคนที่พร้อมจะช่วยเราจากปัญหาบลูทูธ ฉันเรียกพวกเขาว่า... สามทหารเสือ

คุณลักษณะ

สาระสำคัญ

เหตุใดคุณจึงควรใส่ใจ

การสแกนแบบพาสซีฟ

ความสามารถในการฟังอุปกรณ์ Bluetooth โดยไม่ต้องตะโกนใส่อุปกรณ์เหล่านั้น

ตอนนี้แอปของคุณเป็นนินจาที่เงียบและประหยัดแบตเตอรี่ได้แล้ว

เหตุผลการสูญเสียพันธบัตร

สุดท้ายนี้ มาดูสาเหตุว่าทำไมการเชื่อมต่อ Bluetooth ของคุณถึงพัง

คุณสามารถหยุดเล่นเกมทายใจและแก้ไขปัญหาการเชื่อมต่อได้จริง

บริการ UUID จากโฆษณา

รับสถิติสำคัญของอุปกรณ์โดยตรงจากโฆษณา

มันเหมือนกับการออกเดทแบบเร็วสำหรับอุปกรณ์บลูทูธ การเชื่อมต่อที่รวดเร็วและมีประสิทธิภาพยิ่งขึ้น

นี่ไม่ใช่แค่การปรับแต่งเล็กน้อยนะ สิ่งเหล่านี้คือการปรับปรุงคุณภาพชีวิตที่จะเปลี่ยนแปลงวิธีการสร้างและแก้ไขข้อบกพร่องของแอปที่ใช้ Bluetooth โดยพื้นฐาน เหมือนกับว่าทีม Android ฟังเสียงร้องของเราเพื่อขอความช่วยเหลือจริงๆ (ฉันรู้ว่าฉันก็ตกใจเหมือนกัน)

ในอีกไม่กี่ส่วนถัดไป เราจะมาทำความรู้จักกับคุณสมบัติใหม่แต่ละอย่างอย่างใกล้ชิดและเป็นส่วนตัว เราจะเจาะลึกโค้ด สำรวจกรณีการใช้งาน และเรียนรู้วิธีควบคุมพลังของมัน ดังนั้น เตรียมพบกับทหารเสือเสือคนแรกของเรา:ประเภทที่แข็งแกร่งและเงียบซึ่งเรียกว่า Passive Scanning

เจาะลึก #1:การสแกนแบบพาสซีฟ

ลองนึกภาพคุณอยู่ในห้องสมุด คุณกำลังมองหาเพื่อน แต่คุณไม่รู้ว่าพวกเขาอยู่ที่ไหน คุณมีสองตัวเลือก:

  • การสแกนที่ใช้งานอยู่: คุณยืนอยู่กลางห้องสมุดแล้วตะโกนว่า "เฮ้ สตีฟ คุณอยู่ที่นี่หรือเปล่า?" วิธีนี้ได้ผล แต่ก็ส่งเสียงดัง ก่อกวน และจะทำให้คุณโดนบรรณารักษ์ไล่ออก (ซึ่งในการเปรียบเทียบนี้ก็คือแบตเตอรี่ของผู้ใช้ของคุณ)

  • การสแกนแบบพาสซีฟ: คุณเดินไปรอบๆ ห้องสมุดอย่างเงียบๆ ฟังเสียงหัวเราะหอบหืดอันเป็นเอกลักษณ์ของเพื่อนของคุณ คุณไม่พูดอะไรสักคำ คุณเพียงแค่ฟัง นี่เป็นการซ่อนตัว มีประสิทธิภาพ และจะไม่ทำให้แบตเตอรี่โซเชียลของคุณหมด (หรือจริง)

เป็นเวลาหลายปีแล้วที่การสแกน Bluetooth ของ Android เป็นสิ่งที่คนตะโกนในห้องสมุด แต่ด้วย AOSP 16 ในที่สุดเราก็สามารถเป็นผู้ฟังอย่างเงียบๆ ได้ นี่คือความมหัศจรรย์ของการสแกนแบบพาสซีฟ

ใช้งานกับ Passive:การเปิดไพ่ทางเทคนิค

ในโลกของ BLE อุปกรณ์ต่างๆ จะส่งข้อมูลเล็กๆ ที่เรียกว่า "โฆษณา" เป็นวิธีของพวกเขาในการพูดว่า "เฮ้ ฉันอยู่นี่ และนี่คือสิ่งที่ฉันทำ!"

  • การสแกนที่ใช้งานอยู่: เมื่อโทรศัพท์ของคุณทำการสแกนอยู่ โทรศัพท์จะได้ยินโฆษณา จากนั้นจะส่ง SCAN_REQ (คำขอสแกน) กลับมา โดยพื้นฐานแล้วมันเป็นการพูดว่า "บอกฉันเพิ่มเติม!" อุปกรณ์ต่อพ่วงจะตอบกลับด้วย SCAN_RSP (การตอบสนองการสแกน) ซึ่งมีข้อมูลเพิ่มเติม

  • การสแกนแบบพาสซีฟ: ด้วยการสแกนแบบพาสซีฟ โทรศัพท์ของคุณจะได้ยินโฆษณา... เท่านี้ก็เรียบร้อย มันไม่ส่งอะไรกลับมาเลย เพียงจดโฆษณาเริ่มต้นและดำเนินการต่อ เป็นการสนทนาทางเดียว

ทำไมต้องเป็นแบบพาสซีฟ? พลังแห่งความเงียบ

แล้วทำไมเรื่องนี้ถึงเป็นเรื่องใหญ่ขนาดนี้? สองคำ:การใช้พลังงาน ทุกครั้งที่วิทยุในโทรศัพท์ของคุณต้องส่งสัญญาณบางอย่าง (เช่น SCAN_REQ) วิทยุจะใช้พลังงาน หากแอปของคุณสแกนหาอุปกรณ์ตลอดเวลา การส่งข้อมูลเล็กๆ น้อยๆ เหล่านั้นก็จะเพิ่มขึ้น และแบตเตอรี่ของผู้ใช้ก็จะจ่ายตามราคา

เมื่อเปลี่ยนไปใช้การสแกนแบบพาสซีฟ คุณกำลังบอกให้วิทยุฟัง ไม่พูด แค่ฟัง สิ่งนี้จะช่วยลดพลังงานที่จำเป็นสำหรับการสแกนลงอย่างมาก ทำให้เป็นโซลูชันที่สมบูรณ์แบบสำหรับแอปที่ต้องตรวจสอบอุปกรณ์ใกล้เคียงในระยะเวลานาน

รหัส:จะเป็น Bluetooth Ninja ได้อย่างไร

แล้วเราจะใช้โหมดซ่อนตัวที่เพิ่งค้นพบนี้ได้อย่างไร? มันง่ายอย่างน่าประหลาดใจ ทั้งหมดนี้ขึ้นอยู่กับการตั้งค่าการสแกนที่คุณใช้เมื่อเริ่มการสแกน

ก่อนหน้านี้ คุณอาจเคยทำสิ่งนี้:

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

ขณะนี้ ด้วย AOSP 16 เรามีตัวเลือกใหม่ หากต้องการเปิดใช้งานการสแกนแบบพาสซีฟ คุณเพียงตั้งค่าประเภทการสแกน:

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

เดี๋ยวก่อน นั่นไม่ถูกต้อง เอกสารระบุว่า SCAN_TYPE_PASSIVE เป็นประเภทการสแกน ไม่ใช่โหมดสแกน และคุณพูดถูก! ฉันขอโทษ ฉันตื่นเต้นเกินไปนิดหน่อย วิธีที่ถูกต้องในการทำเช่นนี้คือการตั้งค่าโหมดสแกนเป็นแบบพาสซีฟ ลองอีกครั้ง

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

เดี๋ยวก่อน นั่นยังไม่ถูกต้องนัก ดูเหมือนว่าฉันจะสายไปแล้ว มาดูม้วนหนังสืออย่างเป็นทางการกันดีกว่า... นี่มัน! ScanSettings.Builder มีวิธีการใหม่ใน Android 16 QPR2 ไม่ใช่ setScanMode แต่เป็นการตั้งค่าใหม่ทั้งหมด

เรามาทำให้มันถูกต้องกันเถอะ นี่คือวิธีที่ถูกต้องในการเปิดใช้งานการสแกนแบบพาสซีฟ:

// 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()

และที่นั่นคุณมีมัน ด้วยบรรทัดเดียว คุณได้เปลี่ยนแอปของคุณจากนักท่องเที่ยวที่เสียงดังและกินแบตเตอรี่เป็นนินจา Bluetooth ที่เงียบและมีประสิทธิภาพ แบตเตอรี่ของผู้ใช้จะขอบคุณ

แน่นอนว่ายังมีข้อแลกเปลี่ยนอยู่ เนื่องจากคุณไม่ได้ส่ง SCAN_REQ คุณจะไม่ได้รับข้อมูลเพิ่มเติมจาก SCAN_RSP แต่สำหรับกรณีการใช้งานหลายๆ กรณี โฆษณาเริ่มแรกคือสิ่งที่คุณต้องการเท่านั้น และการประหยัดพลังงานมีมากกว่าความคุ้มค่า

ตอนนี้เราเชี่ยวชาญศิลปะการสแกนแบบเงียบแล้ว มาดูปริศนาชิ้นต่อไปกันดีกว่า:ทำความเข้าใจ BluetoothLeScanner เอง

ทำความเข้าใจ BluetoothLeScanner (ดาวเด่นในการแสดงของเรา)

ก่อนที่เราจะสามารถเชี่ยวชาญศิลปะการสแกน Bluetooth ได้อย่างแท้จริง เราต้องเข้าใจอาวุธหลักของเราก่อน นั่นก็คือ BluetoothLeScanner ให้คิดว่ามันเป็น PKE Meter จาก Ghostbusters เป็นเครื่องมือที่เราใช้ในการตรวจจับพลังงานที่มองไม่เห็น (ในกรณีของเราคือโฆษณา BLE) ที่ลอยอยู่รอบตัวเรา แต่อุปกรณ์ล่าผีนี้ทำงานอย่างไรจริงๆ

สถาปัตยกรรม:มองหลังม่าน

ในระดับสูง กระบวนการค่อนข้างตรงไปตรงมา แอปของคุณใช้ชีวิตอย่างสะดวกสบายในโลกใบเล็กๆ ของตัวเอง และตัดสินใจว่าต้องการค้นหาอุปกรณ์ BLE บางอย่าง มันหยิบตัวอย่าง BluetoothLeScanner แล้วพูดว่า "เฮ้ ไปหาของกันเถอะ"

ภายใต้ประทุนมีหลายอย่างเกิดขึ้น BluetoothLeScanner พูดคุยกับ Android Bluetooth Stack (ชื่อรหัสว่า "Fluoride" ซึ่งฟังดูเหมือนเป็นสิ่งที่ทันตแพทย์ของคุณภาคภูมิใจมาก) จากนั้นสแต็กจะสื่อสารกับตัวควบคุม Bluetooth ของอุปกรณ์ ซึ่งเป็นฮาร์ดแวร์จริงที่ทำหน้าที่ส่งและรับคลื่นวิทยุ เป็นกรณีคลาสสิกของ "มันซับซ้อนกว่าที่เห็น"

ซุปตัวอักษร:GATT, GAP และผองเพื่อน

เมื่อคุณเข้าสู่โลกของ BLE คุณจะพบกับคำย่อมากมายอย่างรวดเร็ว อย่าตื่นตกใจ! พวกเขาไม่ได้น่ากลัวอย่างที่คิด สองสิ่งที่สำคัญที่สุดที่ต้องเข้าใจคือ GAP และ GATT

  • GAP (โปรไฟล์การเข้าถึงทั่วไป): ทั้งหมดนี้เกี่ยวกับวิธีที่อุปกรณ์ค้นพบและเชื่อมต่อถึงกัน คิดว่า GAP เป็นคนโกหกที่ไนท์คลับ มันตัดสินใจว่าใครจะได้คุยกับใคร จัดการโฆษณา (อุปกรณ์ตะโกนว่า "ฉันอยู่นี่!") และการสแกน (แอปของคุณกำลังฟังเสียงตะโกนเหล่านั้น) BluetoothLeScanner ของเราเป็นผู้เล่นหลักในข้อ GAP

  • GATT (โปรไฟล์แอตทริบิวต์ทั่วไป): เมื่ออุปกรณ์ทั้งสองเชื่อมต่อกัน GATT จะเข้าควบคุม มันกำหนดวิธีที่พวกเขาแลกเปลี่ยนข้อมูล คิดว่า GATT เป็นบทสนทนาที่เกิดขึ้นจริงในไนท์คลับ มันคือทั้งหมดที่เกี่ยวกับบริการ คุณลักษณะ และคำอธิบาย อุปกรณ์อาจมี "บริการอัตราการเต้นของหัวใจ" ซึ่งมี "คุณลักษณะการวัดอัตราการเต้นของหัวใจ" แอปของคุณอ่านหรือเขียนคุณลักษณะเหล่านี้เพื่อรับข้อมูลที่ต้องการ

เพื่อวัตถุประสงค์ในการสแกน ส่วนใหญ่แล้วเราอาศัยอยู่ในโลกของ GAP พวกเราคือคนที่ยืนอยู่นอกคลับ กำลังฟังโฆษณาที่น่าสนใจ

วงจรชีวิตการสแกน:ละครสามองก์

ชีวิตของการสแกนด้วยบลูทูธนั้นเรียบง่ายแต่งดงาม

  • พระราชบัญญัติที่ 1: การเตรียมการ แอปของคุณจะตัดสินใจว่าถึงเวลาสแกนแล้ว โดยจะได้รับ BluetoothLeScanner สร้างชุด ScanFilters (เพื่อค้นหาเฉพาะอุปกรณ์ที่ระบุเท่านั้น) และ ScanSettings (เพื่อกำหนดวิธีการสแกน เช่น โหมดพาสซีฟใหม่ของเรา) และกำหนด ScanCallback

  • องก์ที่ 2: การสแกน แอปของคุณเรียก startScan() วิทยุ Bluetooth มีชีวิตชีวาพร้อมฟังโฆษณาที่ตรงกับตัวกรองของคุณ เมื่อพบแอปดังกล่าว ระบบจะรายงานกลับไปยังแอปของคุณผ่านเมธอด onScanResult() ใน ScanCallback ของคุณ

  • องก์ที่ 3: จุดจบ. เมื่อแอปของคุณมีเพียงพอแล้ว (หรือที่สำคัญกว่านั้นคือ เมื่อคุณพบสิ่งที่คุณกำลังมองหาแล้ว) แอปจะเรียก stopScan() วิทยุดับลง และทุกอย่างก็เงียบสงบอีกครั้ง สิ่งสำคัญคือต้องหยุดการสแกนทุกครั้งเมื่อเสร็จสิ้น การสแกนอันธพาลเป็นสาเหตุอันดับหนึ่งของการร้องเรียน "แบตเตอรี่ของฉันหมดในหนึ่งชั่วโมง" จากผู้ใช้

และนั่นคือ BluetoothLeScanner โดยสรุป เป็นประตูสู่โลกแห่งการค้นพบ BLE มันทรงพลังและซับซ้อน แต่ในขณะที่เรากำลังเรียนรู้ Android ใหม่ทุกรุ่นก็ฉลาดขึ้นและมีประสิทธิภาพมากขึ้น ตอนนี้เรารู้จักเครื่องมือของเราแล้ว มาเริ่มสร้างเครื่องสแกนแบบพาสซีฟเครื่องแรกกันดีกว่า!

ลงมือปฏิบัติ:สร้าง Passive Scanner เครื่องแรกของคุณ

ทฤษฎีนั้นดีมาก แต่บอกตามตรงว่าเราเป็นนักพัฒนา เราเรียนรู้โดยการทำ (หรือโดยการคัดลอกการวางจาก Stack Overflow) ถึงเวลาที่จะพับแขนเสื้อของเรา ใช้งาน Android Studio และสร้างบางสิ่งขึ้นมา เรากำลังจะสร้างแอปง่ายๆ ที่ใช้ความสามารถในการสแกนแบบพาสซีฟที่เราค้นพบใหม่เพื่อค้นหาอุปกรณ์ BLE ที่อยู่ใกล้เคียง

ขั้นตอนที่ 1:การสอบสวนการอนุญาต

ก่อนที่เราจะเขียน Kotlin บรรทัดเดียว เราต้องเอาใจเทพแห่งการอนุญาตของ Android ก่อน นี่เป็นพิธีกรรมที่ศักดิ์สิทธิ์และมักจะน่าหงุดหงิด สำหรับการสแกนบลูทูธ กฎมีการเปลี่ยนแปลงเล็กน้อยในช่วงหลายปีที่ผ่านมา

ขั้นแรก เปิด 01 ของคุณ และเพิ่มสิ่งต่อไปนี้:

<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" />

เมื่อดูการอนุญาตที่เราประกาศไว้ข้างต้น คุณจะเห็นวิวัฒนาการของโมเดลการอนุญาต Bluetooth ของ Android ที่เล่นแบบเรียลไทม์

สิทธิ์สองรายการแรก 10 และ 24 เป็นยามเก่า มีมาตั้งแต่ยุคแรกๆ ของ Android และมีฟังก์ชัน Bluetooth พื้นฐานและความสามารถในการค้นหาอุปกรณ์ จากนั้นเราก็มี 33 ซึ่งเปิดตัวใน Android 12 (API 31) และแสดงให้เห็นถึงการเปลี่ยนแปลงครั้งสำคัญของวิธีที่ Google คิดเกี่ยวกับความเป็นส่วนตัว

ใช่ คุณเห็นถูกต้องแล้ว ในสมัยก่อน (ก่อน Android 12) Google ตัดสินใจว่าการค้นหาอุปกรณ์บลูทูธโดยพื้นฐานแล้วเหมือนกับการทราบตำแหน่งที่แน่นอนของผู้ใช้ มันค่อนข้างสมเหตุสมผล เพราะหากคุณเห็นว่าบีคอนบลูทูธตัวไหนอยู่ใกล้ๆ คุณก็จะสามารถระบุตำแหน่งของคุณเป็นรูปสามเหลี่ยมได้ แต่มันก็ค่อนข้างน่าขนลุกเช่นกันที่จะขอสถานที่เพียงเพื่อหาหูฟัง สิ่งนี้นำไปสู่สถานการณ์ที่น่าอึดอัดใจที่ผู้ใช้จะเห็นแอปสแกนเนอร์บลูทูธธรรมดาขอตำแหน่งที่แม่นยำและเกิดความสงสัย

โชคดีที่ Android 12 เปิดตัว 48 การอนุญาตซึ่งสมเหตุสมผลกว่ามาก การอนุญาตนี้ทำให้แอปสแกนหาอุปกรณ์บลูทูธได้ในที่สุดโดยไม่จำเป็นต้องขอเข้าถึงตำแหน่ง ซึ่งสมเหตุสมผลกว่ามากจากมุมมองของผู้ใช้ คุณยังคงต้องขอสิทธิ์นี้ขณะใช้งานจริง แต่อย่างน้อยคุณก็ไม่จำเป็นต้องอธิบายให้ผู้ใช้ทราบว่าทำไมแอปค้นหาอุปกรณ์ธรรมดาของคุณต้องการทราบว่าพวกเขาอาศัยอยู่ที่ไหน

อย่างไรก็ตาม ให้สังเกตสิทธิ์สองรายการสุดท้ายสำหรับการเข้าถึงตำแหน่ง สิ่งเหล่านี้คือสิ่งที่หลงเหลืออยู่ของระบบเก่า หากคุณกำลังสร้างแอปที่ต้องรองรับอุปกรณ์รุ่นเก่าที่ใช้ Android 11 หรือต่ำกว่า คุณจะต้องเก็บสิทธิ์เข้าถึงตำแหน่งเหล่านี้ไว้ในไฟล์ Manifest เพื่อความเข้ากันได้แบบย้อนหลัง ในอุปกรณ์สมัยใหม่ 52 การอนุญาตเพียงอย่างเดียวจะทำงานได้

ขั้นตอนที่ 2:รหัสตื่นขึ้น

เอาล่ะ เรามาเข้าสู่ส่วนที่สนุกกันดีกว่า ต่อไปนี้คือรายละเอียดเกี่ยวกับวิธีการใช้งานเครื่องสแกนแบบพาสซีฟในกิจกรรมหรือแฟรกเมนต์ของคุณ

รับเครื่องสแกน

ก่อนอื่น เราต้องได้รับอินสแตนซ์ของ 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
}

เรามาดูรายละเอียดสิ่งที่เกิดขึ้นในโค้ดด้านบนกันดีกว่า เรากำลังใช้ 67 ของ Kotlin การมอบหมาย ซึ่งเป็นวิธีที่หรูหราในการพูดว่า "อย่าสร้างวัตถุนี้จนกว่าฉันจะต้องการมันจริงๆ" นี่เป็นแนวทางปฏิบัติที่ดีเนื่องจากการได้รับอะแดปเตอร์ Bluetooth เกี่ยวข้องกับการเรียกของระบบ และไม่มีประโยชน์ในการทำงานนั้นถ้าเราไม่เคยใช้มันจริงๆ

ขั้นแรก เราคว้า 78 จากบริการของระบบ ลองนึกถึง 87 เป็นผู้เฝ้าประตูทุกสิ่งที่ Bluetooth บนอุปกรณ์ของคุณ จากผู้จัดการรายนี้ เราได้รับ 99 ซึ่งแสดงถึงฮาร์ดแวร์บลูทูธทางกายภาพของอุปกรณ์ของคุณ โปรดสังเกตว่าเรากำลังประกาศว่าเป็นโมฆะ (100 ) เพราะเชื่อหรือไม่ว่าไม่ใช่ทุกอุปกรณ์ Android จะมีบลูทูธ แท็บเล็ตหรืออุปกรณ์ปิดบังบางเครื่องอาจไม่มีฮาร์ดแวร์ ดังนั้นเราจึงต้องเตรียมพร้อมสำหรับความเป็นไปได้นั้น

เมื่อได้อะแดปเตอร์แล้ว เราก็สามารถขอ 113 ได้ . นี่คือวัตถุจริงที่เราจะใช้เพื่อทำการสแกนของเรา ขอย้ำอีกครั้งว่าเรากำลังใช้โอเปอเรเตอร์การโทรที่ปลอดภัย (127 ) เพราะหากอะแดปเตอร์เป็นโมฆะ (ไม่มีฮาร์ดแวร์ Bluetooth) เราจะไม่สามารถรับสแกนเนอร์จากอะแดปเตอร์นั้นได้อย่างแน่นอน การเขียนโปรแกรมเชิงป้องกันนี้อาจดูหวาดระแวง แต่มันคือสิ่งที่แยกแอปที่ขัดข้องอย่างลึกลับออกจากแอปที่จัดการกับ Edge Case ได้อย่างงดงาม

กำหนดการเรียกกลับ

นี่คือจุดที่ความมหัศจรรย์เกิดขึ้น ScanCallback เป็นวัตถุที่จะรับฟังผลการสแกน เราจำเป็นต้องแทนที่สองวิธี:onScanResult และ 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")
 }
}

137 เราได้กำหนดไว้ข้างต้นคือหูของแอปของคุณในโลก Bluetooth เมื่อเครื่องสแกนพบอุปกรณ์ เครื่องสแกนจะไม่เพียงแค่จัดเก็บข้อมูลไว้ที่ใดที่หนึ่งเท่านั้น แต่ยังโทรกลับไปยังแอปของคุณผ่านออบเจ็กต์การโทรกลับนี้อีกด้วย นี่คือการเขียนโปรแกรมที่ขับเคลื่อนด้วยเหตุการณ์แบบคลาสสิก และเป็นวิธีที่ Android ช่วยให้แอปของคุณตอบสนองโดยไม่ปิดกั้นเธรดหลัก

149 วิธีการนี้จะถูกเรียกใช้ทุกครั้งที่สแกนเนอร์ค้นพบอุปกรณ์ที่ตรงกับตัวกรองของคุณ (หรืออุปกรณ์ใดๆ หากคุณไม่ได้ใช้ตัวกรอง) 151 พารามิเตอร์เป็นขุมสมบัติของข้อมูล ประกอบด้วย 160 วัตถุ (ซึ่งมีที่อยู่ MAC และชื่อของอุปกรณ์), ค่า RSSI (ตัวบ่งชี้ความแรงของสัญญาณที่ได้รับ – โดยพื้นฐานแล้วอุปกรณ์อยู่ใกล้แค่ไหน โดยตัวเลขที่สูงกว่าหมายถึงอยู่ใกล้มากขึ้น) และข้อมูลโฆษณาดิบที่อุปกรณ์กำลังออกอากาศ

ในตัวอย่างง่ายๆ ข้างต้น เราแค่บันทึกที่อยู่ MAC และ RSSI แต่ในแอปจริง คุณอาจต้องการอัปเดต UI ของคุณ เพิ่มอุปกรณ์ลงในรายการ หรือทริกเกอร์การเชื่อมต่อ

175 พารามิเตอร์จะบอกคุณว่า ทำไม การโทรกลับนี้ถูกทริกเกอร์ อาจเป็น 188 (ค่าเริ่มต้นหมายถึง "นี่คืออุปกรณ์ทุกเครื่องที่เราพบ") 192 (ครั้งแรกที่เราเห็นอุปกรณ์นี้) หรือ 206 (เราไม่ได้เห็นเครื่องนี้มาสักพักแล้ว คงจะเหลือ) เราจะเจาะลึกประเภทเหล่านี้ในส่วนขั้นสูง

จากนั้นก็มี 210 วิธีที่เราทุกคนหวังว่าจะไม่ถูกเรียกแต่เราต้องจัดการอย่างแน่นอน สิ่งนี้จะถูกเรียกใช้เมื่อมีข้อผิดพลาดร้ายแรงเกิดขึ้นกับการสแกน บางทีอะแดปเตอร์บลูทูธอาจปิดอยู่ในระหว่างการสแกน แอพของคุณอาจไม่มีสิทธิ์ที่เหมาะสม หรือบางทีตัวควบคุมบลูทูธอาจมีวันที่แย่ 228 จะให้คำแนะนำเกี่ยวกับสิ่งที่ผิดพลาด และคุณควรบันทึกสิ่งนี้และจัดการกับมันอย่างดีเสมอ โดยอาจแสดงข้อความให้ผู้ใช้เห็นหรือพยายามเริ่มการสแกนใหม่หลังจากเกิดความล่าช้า

กำหนดค่าการสแกน

ตอนนี้เราสร้าง ScanSettings ของเรา นี่คือที่ที่เราบอก Android ว่าเราอยากเป็นนินจาที่เฉื่อยชาและประหยัดแบตเตอรี่

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()

236 วัตถุที่เรากำลังสร้างด้านบนนั้นเหมือนกับคู่มือการใช้งานโดยละเอียดสำหรับเครื่องสแกนบลูทูธ แต่ละวิธีจะเรียกการปรับแต่งอย่างละเอียดว่าการสแกนควรทำงานอย่างไร และการตั้งค่าเหล่านี้ให้ถูกต้องคือความแตกต่างระหว่างแอปที่เป็นมิตรกับแบตเตอรี่กับแอปที่จะถอนการติดตั้งภายในไม่กี่ชั่วโมง

มาดูการตั้งค่าแต่ละอย่างกัน อันดับแรก 243 บอกให้เครื่องสแกนใช้โหมดการสแกนที่ใช้พลังงานต่ำ ซึ่งหมายความว่าเครื่องจะสแกนเป็นช่วง ๆ แทนที่จะสแกนแบบต่อเนื่อง เหมาะสำหรับกรณีการใช้งานส่วนใหญ่ที่คุณไม่ต้องการผลลัพธ์ทันทีและต้องการรักษาอายุการใช้งานแบตเตอรี่ เครื่องสแกนจะตื่น สแกนสักพัก เข้าสู่โหมดสลีป และทำซ้ำ บลูทูธเทียบเท่ากับการงีบหลับพลังงาน

ถัดไป 254 หมายความว่าเราต้องการได้รับการแจ้งเตือนทุกครั้งที่เครื่องสแกนพบอุปกรณ์ที่ตรงกัน นี่คือลักษณะการทำงานเริ่มต้นและคือสิ่งที่คุณจะใช้เป็นส่วนใหญ่ ดังที่เราได้กล่าวไปแล้ว คุณยังสามารถใช้ 264 ได้อีกด้วย หรือ 274 เพื่อการตรวจจับสถานะที่ซับซ้อนยิ่งขึ้น

285 การตั้งค่าจะควบคุมว่าฮาร์ดแวร์ควรพยายามจับคู่อุปกรณ์กับตัวกรองของคุณอย่างจริงจังเพียงใด 292 หมายถึง "รายงานการจับคู่อย่างรวดเร็ว แม้ว่าคุณจะไม่แน่ใจ 100%" ในขณะที่ 301 หมายถึง “รอจนแน่ใจจริงๆ ก่อนจึงจะรายงาน” โหมดก้าวร้าวช่วยให้คุณได้ผลลัพธ์เร็วขึ้น แต่บางครั้งอาจให้ผลบวกลวง

จากนั้นเราก็มี 312 ซึ่งบอกให้สแกนเนอร์รายงานอุปกรณ์หลังจากเห็นโฆษณาเพียงรายการเดียว ทางเลือกอื่นคือ 328 ซึ่งรอโฆษณาหลายรายการก่อนที่จะรายงาน การใช้โฆษณาชิ้นเดียวช่วยให้คุณค้นพบได้เร็วขึ้น ในขณะที่รอสักครู่จะช่วยลดผลบวกลวงจากอุปกรณ์ที่เพิ่งผ่านไป

331 การตั้งค่าเป็นสิ่งสำคัญ ความล่าช้าของ 340 หมายถึง “รายงานผลทันที” หากคุณตั้งค่านี้เป็น ให้พูด 353 มิลลิวินาที เครื่องสแกนจะรวบรวมผลลัพธ์และส่งทุกๆ 5 วินาที การรวมกลุ่มเหมาะอย่างยิ่งสำหรับการสแกนพื้นหลัง (ดังที่เราได้กล่าวไว้ในส่วนขั้นสูง) แต่สำหรับการสแกนเบื้องหน้าซึ่งผู้ใช้กำลังรออยู่ การรายงานทันทีคือสิ่งที่คุณต้องการ

และสุดท้าย สตาร์เด่นในรายการของเรา:364 . นี่คือ API ใหม่จาก Android 16 QPR2 ที่เปลี่ยนเครื่องสแกนของเราให้เป็นผู้ฟังแบบไม่มีเสียง แทนที่จะส่งคำขอสแกนไปยังอุปกรณ์ทุกเครื่องที่ได้ยิน แต่เพียงฟังโฆษณาที่ลอยอยู่ในอากาศ การตั้งค่าเดียวนี้สามารถลดการใช้พลังงานแบตเตอรี่ของแอประหว่างการสแกนได้อย่างมาก เป็นคุณสมบัติที่เรารอคอยและยอดเยี่ยมมาก

เริ่มและหยุดการสแกน

สุดท้ายนี้ เราจำเป็นต้องมีฟังก์ชันเพื่อเริ่มและหยุดการสแกนของเรา ข้อควรจำ:หยุดการสแกนของคุณเสมอ! การสแกนที่ถูกลืมคือสัตว์ประหลาดที่กินแบตเตอรี่

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.")
 }
}

ฟังก์ชันทั้งสองข้างต้นนี้เป็นสวิตช์เปิด/ปิดสำหรับเครื่องสแกนบลูทูธของคุณ และมันก็ดูเรียบง่ายจนเข้าใจผิดว่าสำคัญแค่ไหน เรามาดูรายละเอียดสิ่งที่เกิดขึ้นในแต่ละเรื่องกันดีกว่า

ใน 375 ก่อนอื่นเราจะตรวจสอบว่า 383 หรือไม่ ไม่เป็นโมฆะ นี่คือเครือข่ายความปลอดภัยของเรา:หากอุปกรณ์ไม่มีฮาร์ดแวร์ Bluetooth หรือหากปิดใช้งาน Bluetooth เครื่องสแกนจะเป็นโมฆะ และเราไม่ต้องการหยุดทำงานด้วยการพยายามเรียกใช้เมธอดบนวัตถุว่าง หากมีเครื่องสแกนอยู่ เราจะเรียก 398 โดยมีพารามิเตอร์ 3 ตัว:รายการ 401 วัตถุ 419 ที่สร้างขึ้นอย่างพิถีพิถันของเรา และ 426 เรากำหนดไว้ก่อนหน้านี้

433 ขณะนี้รายการว่างเปล่าในตัวอย่างของเรา ซึ่งหมายถึง "ค้นหาอุปกรณ์ BLE ทั้งหมด" ในแอปที่มีการใช้งานจริง โดยทั่วไปคุณจะเพิ่มตัวกรองที่นี่เพื่อจำกัดการค้นหาของคุณให้แคบลง

ตัวอย่างเช่น หากคุณกำลังสร้างแอปที่ใช้งานได้กับเครื่องวัดอัตราการเต้นของหัวใจเท่านั้น คุณจะต้องสร้างตัวกรองที่ตรงกับอุปกรณ์ที่โฆษณา UUID ของบริการอัตราการเต้นของหัวใจเท่านั้น นี่เป็นสิ่งสำคัญสำหรับทั้งประสิทธิภาพและอายุการใช้งานแบตเตอรี่:เหตุใดจึงปลุกแอปของคุณสำหรับแปรงสีฟัน Bluetooth แบบสุ่มทุกอัน ในเมื่อคุณสนใจแต่ตัวติดตามฟิตเนสเท่านั้น

443 วิธีการเริ่มต้นกระบวนการสแกน จากจุดนี้ไป วิทยุ Bluetooth จะกระตือรือร้น (หรือในกรณีของเรา เฉยๆ) กำลังฟังโฆษณา และ 457 ของคุณ จะเริ่มได้รับผล นี่เป็นการดำเนินการแบบอะซิงโครนัส ซึ่งหมายความว่าโค้ดของคุณไม่ได้บล็อกอยู่ที่นี่เพื่อรอผลลัพธ์ แต่จะยังคงดำเนินการต่อไป และผลลัพธ์จะเข้ามาทางการโทรกลับทุกครั้งที่พร้อมใช้งาน

ตอนนี้เรามาพูดถึง 463 ซึ่งอาจเป็นฟังก์ชันที่สำคัญที่สุดที่คุณเขียน เมื่อคุณโทร 474 ด้วยการโทรกลับ คุณกำลังบอกวิทยุบลูทูธว่า "โอเค เสร็จแล้ว คุณกลับไปนอนได้" การดำเนินการนี้จะหยุดกระบวนการสแกนทันทีและปล่อยทรัพยากร

สิ่งสำคัญที่ต้องทำความเข้าใจคือถ้าคุณไม่เรียกสิ่งนี้ การสแกนจะทำงานต่อไปอย่างไม่มีกำหนด ซึ่งจะทำให้แบตเตอรี่ของผู้ใช้หมดเหมือนแวมไพร์ในธนาคารเลือดที่กินได้ไม่อั้น นี่คือเหตุผลที่เราเน้นย้ำมันมาก:486 ที่ถูกลืม การโทรเป็นหนึ่งในสาเหตุที่พบบ่อยที่สุดของปัญหาแบตเตอรี่หมดในแอป Bluetooth

โปรดสังเกตว่าเรากำลังส่ง 498 เดียวกัน คัดค้าน 502 ที่เราใช้ใน 515 . นี่คือวิธีที่ Android รู้ว่าการสแกนใดที่จะหยุด - ตามทฤษฎีแล้ว คุณอาจมีการสแกนหลายรายการที่ทำงานโดยมีการโทรกลับต่างกัน (แม้ว่าจะไม่ค่อยเป็นความคิดที่ดีก็ตาม) ตรวจสอบให้แน่ใจว่าคุณกำลังหยุดการสแกนแบบเดิมที่คุณเริ่มต้นโดยใช้การอ้างอิงการโทรกลับเดียวกัน

นำทุกอย่างมารวมกัน

นี่คือตัวอย่างที่สมบูรณ์ที่คุณสามารถใส่ลงในกิจกรรมได้ เพียงจำไว้ว่าต้องจัดการสิทธิ์รันไทม์!

// 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()
 }
}

ตัวอย่างที่สมบูรณ์ด้านบนแสดงให้เห็นว่าชิ้นส่วนทั้งหมดประกอบเข้าด้วยกันในกิจกรรมจริงได้อย่างไร นี่เป็นสแกนเนอร์ Bluetooth ที่เรียบง่ายแต่ใช้งานได้จริงซึ่งคุณสามารถใช้งานได้จริง เรามาเน้นรูปแบบที่สำคัญบางประการที่เราใช้ที่นี่

ขั้นแรก ให้สังเกตว่าเราเชื่อมโยงวงจรการสแกนกับการกระทำของผู้ใช้ผ่านการคลิกปุ่มอย่างไร นี่เป็นรูปแบบทั่วไป:ผู้ใช้จะเริ่มและหยุดการสแกนอย่างชัดเจน ทำให้สามารถควบคุมเวลาที่แอปใช้บลูทูธได้ นี่เป็นทั้ง UX ที่ดีและดีสำหรับอายุการใช้งานแบตเตอรี่ เนื่องจากการสแกนจะทำงานเมื่อผู้ใช้ต้องการเท่านั้น

แต่นี่คือส่วนที่สำคัญมาก:522 แทนที่ นี่คือตาข่ายนิรภัยที่สำคัญ เมื่อกิจกรรมของคุณเข้าสู่เบื้องหลัง (ผู้ใช้อาจกดปุ่มโฮมหรือเปลี่ยนไปใช้แอปอื่น) 530 ถูกเรียกและเราหยุดการสแกนทันที นี่ถือเป็นสิ่งสำคัญ เนื่องจากหากผู้ใช้ไม่เห็นแอปของคุณ ก็ไม่จำเป็นต้องได้รับผลการสแกน และไม่มีเหตุผลที่จะต้องใช้แบตเตอรี่จนหมด รูปแบบนี้ช่วยให้แน่ใจว่าแม้ว่าผู้ใช้ลืมกดปุ่ม "หยุด" การสแกนจะไม่ทำงานในพื้นหลังตลอดไป

คุณอาจสงสัยว่า "แล้ว 545 ล่ะ ? เราไม่ควรเริ่มการสแกนใหม่เมื่อผู้ใช้กลับมาหรือ?” นั่นเป็นการตัดสินใจในการออกแบบ ในบางแอป คุณอาจต้องการรีสตาร์ทการสแกนโดยอัตโนมัติใน 559 . ในกรณีอื่นๆ คุณอาจต้องการให้ผู้ใช้กด "Start" อีกครั้งอย่างชัดเจน ขึ้นอยู่กับกรณีการใช้งานของคุณ สำหรับแอปค้นหาอุปกรณ์ที่ผู้ใช้กำลังค้นหาอยู่ การกลับมาทำงานต่ออัตโนมัติก็สมเหตุสมผล สำหรับแอปตรวจสอบที่ทำงานในเบื้องหลัง คุณอาจต้องการการควบคุมที่ชัดเจนยิ่งขึ้น

สิ่งสำคัญอย่างหนึ่งที่เราไม่ได้แสดงในตัวอย่างนี้คือการจัดการสิทธิ์รันไทม์ จำสิทธิ์เหล่านั้นที่เราประกาศในรายการหรือไม่ บน Android 6.0 ขึ้นไป คุณไม่สามารถประกาศได้เพียงอย่างเดียว แต่คุณต้องขอจากผู้ใช้ในขณะรันไทม์ ก่อนโทร 569 คุณควรตรวจสอบว่าคุณมีสิทธิ์ที่จำเป็นหรือไม่ และหากไม่มี ให้ร้องขอโดยใช้ 575 . หากคุณพยายามเริ่มการสแกนโดยไม่มีการอนุญาตที่เหมาะสม การสแกนจะล้มเหลวโดยไม่มีการแจ้งเตือน (หรือเสียงดัง ขึ้นอยู่กับเวอร์ชันของ Android) และคุณคงสงสัยว่าทำไมไม่มีอะไรทำงานเลย

และคุณก็ได้แล้ว! คุณเพิ่งสร้างเครื่องสแกนบลูทูธแบบพาสซีฟ AOSP 16 เครื่องแรกของคุณ มันบาง มันใจร้าย และประหยัดพลังงานอย่างเหลือเชื่อ เครื่องสแกนจะฟังโฆษณา BLE อย่างเงียบๆ รายงานโฆษณาผ่านการโทรกลับของคุณ และหยุดอย่างสง่างามเมื่อไม่จำเป็น

ตอนนี้เรามาดูหัวข้อถัดไปกันดีกว่า:จะทำอย่างไรเมื่อมีสิ่งผิดปกติเกิดขึ้น ถึงเวลาที่จะพูดคุยเกี่ยวกับการเลิกรา... การเลิกราของบลูทูธ นั่นคือ

เจาะลึก #2:เหตุผลในการสูญเสียพันธบัตร Bluetooth

อา พันธะบลูทูธ เป็นสิ่งสวยงามและศักดิ์สิทธิ์ มันเทียบเท่ากับการแลกเปลี่ยนกำไลมิตรภาพแบบดิจิทัล เมื่อคุณเชื่อมต่อโทรศัพท์เข้ากับหูฟัง คุณกำลังสร้างความสัมพันธ์ระยะยาวและเชื่อถือได้ พวกเขาแชร์คีย์ลับ จดจำกันและกัน และสัญญาว่าจะเชื่อมต่อโดยอัตโนมัติ ช่วยให้คุณไม่ต้องยุ่งยากในการจับคู่ทุกครั้ง เป็นความโรแมนติกที่สวยงาม

จนกว่าจะไม่ใช่

จู่ๆ วันหนึ่งพวกเขาก็...ลืมกัน การเชื่อมต่อหายไป ความไว้วางใจถูกทำลาย และแอปของคุณถูกทิ้งไว้ตรงกลาง พยายามเล่นนักบำบัดโดยไม่รู้ว่าเกิดอะไรขึ้น คุณถูกผี และจนถึงขณะนี้ Android ก็ยังไม่มีความช่วยเหลือใด ๆ คุณจะได้รับการแจ้งเตือนว่าสถานะพันธบัตรเป็น BOND_NONE แต่ก็แค่นั้น ไม่มีคำอธิบาย ไม่มีการปิด. เป็นเพียงความเงียบอันเยือกเย็นและแข็งกระด้างของการเชื่อมต่อที่ล้มเหลว

ในที่สุดก็ปิดตัวลง!

แต่เพื่อนของเราในทีม Android ต้องผ่านการเลิกราที่ยากลำบากมาอย่างชัดเจน เพราะใน AOSP 16 พวกเขามอบของขวัญแห่งการปิดฉากให้กับเรา ขอแนะนำอุปกรณ์ Bluetooth EXTRA_BOND_LOSS_REASON เป็นส่วนเสริมใหม่ที่มาพร้อมกับการออกอากาศ ACTION_BOND_STATE_CHANGED และที่นี่เพื่อบอกคุณว่าทำไมความสัมพันธ์จึงหายไป มันเหมือนกับได้รับข้อความบอกเลิกที่อธิบายสิ่งที่เกิดขึ้นจริงๆ!

ตอนนี้ เมื่อพันธบัตรขาด คุณจะได้รับรหัสเหตุผลเฉพาะ คิดว่าสิ่งเหล่านี้เป็นข้อแก้ตัวในการเลิกราแบบคลาสสิก แต่สำหรับบลูทูธ:

รหัสเหตุผล (ภาพประกอบ)

ความหมายที่แท้จริงหมายถึงอะไร

BOND_LOSS_REASON_BREDR_AUTH_FAILURE

บ่งชี้ว่าสาเหตุของการสูญเสียพันธบัตรคือความล้มเหลวในการรับรองความถูกต้องของ BREDR

BOND_LOSS_REASON_BREDR_INCOMING_PAIRING

บ่งชี้ว่าสาเหตุของการสูญเสียพันธบัตรคือความล้มเหลวในการจับคู่ BREDR

BOND_LOSS_REASON_LE_ENCRYPT_FAILURE

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

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 581 , 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 594 method, we first check if the incoming intent's action is 602 . 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 612 and 627 .

The 632 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 645 tells us the current state (are we bonded, bonding, or not bonded?), and 653 tells us what the state was before this change occurred.

The key logic happens in our conditional check:667 . 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 676 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 681 and unregister it in 695 . 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 700 and unregistering in 718 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 727 object contains a 730 property. 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 743 and get back a nice, clean list of 756 วัตถุ The safe call operator (760 ) 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:778 . 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 789 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 794 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 803 , which tells the filter to only match devices that advertise this specific service.

But the builder has many other options you can use:

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

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

  • 835 – Match devices based on manufacturer-specific data in their advertisements

  • 849 – 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 850 along with our scan settings and callback. This is where the magic happens. 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 862 , 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 870 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 886 parameter in 897 is a 908 , and each 917 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.

คำเตือน: 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 928 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 (933 in Kotlin, 944 in Java), you're telling the Bluetooth hardware to track the presence state of devices over time. The code 955 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 961 event. This is different from 974 (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 984 , you get one notification when the device enters your scanning range, not a flood of notifications as it continues to advertise.

The 997 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 1007 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 1018 callback, but this delay is usually acceptable for presence detection use cases.

In the code example above, we're using a 1026 expression to handle the different callback types. When we receive a 1031 , 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 1044 , 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:1053 and 1060 are mutually exclusive with 1076 . If you combine them with 1086 , the behavior becomes undefined and varies by device. Stick to either 1090 for continuous reporting, or 1106 /1117 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 1129 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

เราทุกคนเคยไปที่นั่น 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:1139 . This constant knows about both the major and minor version numbers. And to go with it, we have a new set of version codes:1146 .

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.

เรียนรู้การเขียนโค้ดฟรี หลักสูตรโอเพ่นซอร์สของ freeCodeCamp ช่วยให้ผู้คนมากกว่า 40,000 คนได้งานในตำแหน่งนักพัฒนา เริ่มต้น