아, 블루투스. 우리 모두가 싫어하는 기술. 마치 항상 연결되려고 했지만... 연결되지 않는 친구와 같습니다.
수년 동안 Android 개발자들은 Bluetooth와의 극적이고 때로는 비극적인 로맨스에 빠져 있었습니다. 우리는 그 이상한 점과 씨름하고, 그냥 작동하도록 애원했으며, 신비한 연결 끊김에 대해 말없는 눈물을 흘렸습니다.
하지만 상황이 곧 좋아질 것이라고 말하면 어떻게 될까요? Android 16을 통해 마침내 블루투스 신들이 우리에게 미소를 지었다고 말하면 어떨까요? 친구들아, 이건 꿈이 아니다. AOSP 16 Bluetooth 스캐너는 지친 개발자 영혼에 새로운 희망을 가져다 줄 것입니다.
이 핸드북에서 우리는 여행을 떠나고 있습니다. AOSP 16의 새로운 Bluetooth 기능의 핵심을 살펴보세요. 우리는 웃고 울고(이번에는 기쁨으로) 이 새로운 힘을 영원히 사용하는 방법을 배우게 될 것입니다. 패시브 스캐닝의 마법, 채권 손실 이유의 드라마, 일반적인 번거로움 없이 서비스 UUID를 얻을 수 있는 순수한 편리함을 살펴보겠습니다.
이 장대한 이야기가 끝나면 다음을 수행할 수 있습니다:
-
매우 효율적이고 사실상 심령술사 같은 Bluetooth 스캐너를 만들어 보세요.
-
노련한 탐정처럼 연결 문제를 디버깅하세요.
-
새로 발견한 Bluetooth 기술로 친구와 동료에게 깊은 인상을 남겨주세요.
전제조건:
시작하기 전에 Android 개발과 Kotlin에 대한 기본적인 이해를 갖추는 것이 좋습니다. 두 개의 장치가 서로 통신하도록 시도하다가 결국 컴퓨터를 창밖으로 던져버리고 싶어진 적이 있다면, 당신은 자격이 충분합니다.
좋아하는 음료수를 들고 코딩 망토를 착용하고 Bluetooth를 깨울 준비를 하세요!
목차
-
Android 블루투스의 간략한 역사
-
AOSP 16의 새로운 기능:삼총사
-
심층 분석 #1:수동 검색
-
BluetoothLeScanner 이해
-
실습:첫 번째 패시브 스캐너 구축
-
심층 분석 #2:블루투스 본드 손실 이유
-
심층 분석 #3:광고에서 UUID 서비스
-
고급 주제:스캔 게임 레벨 업
-
실제 사용 사례:Bluetooth가 출시되는 곳
-
API 버전 확인:앱이 충돌하지 않는 방법
-
테스트 및 디버깅:재미있는 부분(아무도 말하지 않았음)
-
성능 및 모범 사례:훌륭한 Bluetooth 시민이 되는 방법
-
결론:미래는 수동적이다(그래도 괜찮다)
블루투스의 간략한 역사(또는:걱정을 멈추고 전파를 사랑하는 법을 배운 방법)
암흑시대:클래식 블루투스
태초에는 클래식 블루투스가 있었습니다. 그것은 시끄럽고 떠들썩한 파티 손님의 디지털 버전이었습니다. 좋아하는 음악을 스피커로 보내는 등 많은 양의 데이터를 전달할 수 있지만 확실히 배터리를 많이 소모합니다. 오디오 스트리밍에는 훌륭했지만 작고 드물게 데이터를 전송하는 데에는 적합했습니다. 그것은 화초에 물을 주기 위해 소방 호스를 사용하는 것과 같았습니다. 과도하고 솔직히 조금 지저분합니다.
이 시대의 개발자들은 BluetoothAdapter, BluetoothDevice 및 두려운 BluetoothSocket과 씨름하면서 하루를 보냈습니다. 간단한 연결에도 몇 초가 걸리거나... 음, 커피 한 잔을 만들러 갈 수 있을 정도로 불확실성이 큰 시대였습니다. 그리고 배터리 소모? 사용자는 휴대전화의 전력 수준이 납 풍선보다 빠르게 떨어지는 것을 보게 될 것입니다.
르네상스:저전력 블루투스(BLE)의 등장
그러다가 Android 4.3에서는 Bluetooth Low Energy(BLE)라는 새로운 영웅이 등장했습니다. 이건 네 아버지의 블루투스가 아니었어. BLE는 매끄럽고 효율적이며 신비했습니다. 짧은 시간 동안 데이터를 들이키는 대신 고급 와인처럼 힘차게 마시도록 설계되었습니다.
BLE는 이 동네의 멋진 아이였습니다. 심박수 모니터, 스마트 시계, 단일 코인 셀 배터리로 몇 달 동안 작동할 수 있는 백만 개의 IoT 장치 등 완전히 새로운 가능성의 세계를 소개했습니다. 획기적인 변화였습니다.
하지만 엄청난 힘이 생겨났고... 엄청난 복잡성이 생겼습니다. 우리는 GATT, GAP, 서비스, 특성 등 완전히 새로운 언어를 배워야 했습니다. 그것은 단순한 대본 작성에서 본격적인 오페라 작곡으로 넘어가는 것과 같았습니다. 잠재력은 엄청났지만 학습 곡선은 가팔랐습니다.
문제아:스캔
그리고 스캔이 있었습니다. 이러한 새로운 전력 소모 장치를 찾는 행위입니다. BLE 초기에는 스캐닝이 여전히 다소 험난한 상황이었습니다. 그것은 활발하고 시끄러운 과정이었습니다. 당신의 전화기는 허공에 "누구 있나요?"라고 소리친 다음 응답을 듣습니다. 이 방법은 효과가 있었지만 여전히 전력 소모가 심했습니다. 특히 앱이 장기간 스캔해야 하는 경우에는 더욱 그렇습니다.
이는 고전적인 개발자의 딜레마였습니다. 장치를 찾아야 하지만 점심시간까지 사용자의 휴대폰이 작동하지 않는 이유가 되고 싶지는 않습니다. 수년 동안 우리는 발견의 필요성과 배터리 수명에 대한 간절한 요구 사이에서 균형을 이루며 줄타기를 했습니다.
이것이 AOSP 16이 탄생한 세상입니다. 더 나은 스캔 방법을 요구하는 세상. 영웅을 맞이할 준비가 된 세상. 그리고 그 영웅은 패시브 스캐닝입니다. 이에 대한 자세한 내용은 잠시 후에...
AOSP 16의 새로운 기능은 무엇인가요? (스포일러:정말 멋지네요)
좋습니다. 좋은 점을 살펴보겠습니다. AOSP 16에서 Android 팀은 어떤 반짝이는 새 장난감을 제공했나요? 꽤 많은 것으로 밝혀졌습니다! 하지만 선물 포장을 풀기 전에 새로운 배송 일정에 대해 이야기해 보겠습니다. 지금은 그것조차 조금 다르기 때문입니다.
두 작품의 이야기
놀랍게도 Android는 2025년에 두 가지 주요 API 릴리스를 제공하기로 결정했습니다. 먼저 메인 이벤트인 Android 16(누가 맛있는 페이스트리를 좋아하지 않기 때문에 코드명 "Baklava")이 2분기에 출시되었습니다. 이것은 여러분이 알고 있고 좋아하는(또는 두려워하는) 모든 행동 변화가 포함된 전통적인 빅뱅 릴리스입니다.
하지만 4분기에 우리는 놀라운 두 번째 막인 마이너 릴리스를 접하게 됩니다. 여기서 새로운 Bluetooth 기능이 본격적으로 등장하게 됩니다. 이번 릴리스에는 무서운 앱 중단 변경 없이 새로운 기능과 API가 모두 포함되어 있습니다. 이는 이미 요금을 지불한 후에 무료 디저트를 받는 것과 같습니다.
블루투스 삼총사
그렇다면 이번 Q4 릴리스가 Bluetooth 파티에 어떤 영향을 미쳤습니까? 물어봐주셔서 기뻐요. Bluetooth 문제에서 우리를 구할 준비가 된 세 명의 새로운 영웅이 등장했습니다. 나는 그들을... 삼총사라고 부릅니다.
기능
요점
주의를 기울여야 하는 이유
수동 검사
소리를 지르지 않고도 Bluetooth 장치의 소리를 들을 수 있는 기능.
이제 귀하의 앱은 조용하고 배터리를 절약하는 닌자가 될 수 있습니다.
채권 손실 이유
마지막으로 블루투스 연결이 끊어지는 이유에 대해 설명합니다.
추측 게임을 중단하고 실제로 연결 문제를 디버깅할 수 있습니다.
광고의 서비스 UUID
광고에서 직접 기기의 필수 통계를 확인하세요.
블루투스 기기의 스피드 데이트와 같습니다. 더욱 빠르고 효율적인 연결.
이것은 단지 사소한 변경이 아닙니다. 이는 Bluetooth 지원 앱을 구축하고 디버깅하는 방법을 근본적으로 변화시키는 삶의 질 향상입니다. 마치 안드로이드 팀이 실제로 도움을 청하는 우리 집단의 외침을 듣는 것과 같습니다. (나도 알고 충격받았어.)
다음 몇 섹션에서는 이러한 새로운 기능 각각을 자세히 살펴보겠습니다. 코드를 자세히 살펴보고, 사용 사례를 살펴보고, 그 기능을 활용하는 방법을 알아봅니다. 따라서 첫 번째 총사인 패시브 스캐닝으로 알려진 강하고 조용한 유형의 총사를 만날 준비를 하세요.
심층 분석 #1:수동 검색
당신이 도서관에 있다고 상상해보십시오. 당신은 친구를 찾고 있지만 그들이 어디에 있는지 모릅니다. 두 가지 옵션이 있습니다:
-
활성 검색: 당신은 도서관 중앙에 서서 “HEY, STEVE! 여기 계세요?”라고 외칩니다. 이는 효과적이지만 시끄럽고 파괴적이며 사서(이 비유에서 사용자의 배터리)에 의해 쫓겨나게 될 것입니다.
-
수동 검사: 당신은 친구의 특유의 쌕쌕거리는 웃음 소리를 들으며 조용히 도서관을 돌아다닙니다. 당신은 한마디도하지 않습니다. 그냥 들어보세요. 이는 은밀하고 효율적이며 소셜(또는 실제) 배터리를 소모하지 않습니다.
수년 동안 Android의 Bluetooth 스캐닝은 도서관에서 소리를 지르는 사람이었습니다. 하지만 AOSP 16을 사용하면 마침내 조용한 청취자가 될 수 있습니다. 이것이 수동 검색의 마법입니다.
액티브 대 패시브:기술 대결
BLE 세계에서 장치는 "광고"라는 작은 정보 패킷을 전송합니다. 이는 "나 여기 있는데 이게 내가 하는 일이야!"라고 말하는 방식입니다.
-
활성 검색: 전화기가 활성 스캔을 수행하면 광고를 듣고 SCAN_REQ(스캔 요청)를 다시 보냅니다. 기본적으로는 "더 말해주세요!"라는 뜻입니다. 그러면 주변 장치는 추가 정보가 포함된 SCAN_RSP(Scan Response)로 응답합니다.
-
수동 검사: 패시브 스캐닝을 사용하면 휴대전화가 광고를 듣게 됩니다... 그게 전부입니다. 아무것도 다시 보내지 않습니다. 초기 광고를 기록하고 계속 진행합니다. 일방적인 대화입니다.
왜 패시브로 전환해야 합니까? 침묵의 힘
그렇다면 이것이 왜 그렇게 큰 일입니까? 두 단어:전력 소비. 휴대폰의 무선 장치가 SCAN_REQ와 같은 무언가를 전송해야 할 때마다 에너지를 사용합니다. 앱이 항상 기기를 검색하는 경우 이러한 작은 전송이 합산되어 사용자의 배터리가 그 대가를 지불하게 됩니다.
패시브 스캐닝으로 전환하면 라디오에 듣기만 하라고 지시하는 것입니다. 말하지 않고 듣기만 합니다. 이는 스캔에 필요한 전력을 대폭 줄여주므로 장기간 근처 기기를 모니터링해야 하는 앱에 완벽한 솔루션입니다.
코드:블루투스 닌자가 되는 방법
그렇다면 이 새로 발견된 스텔스 모드를 어떻게 구현합니까? 놀라울 정도로 간단합니다. 모든 것은 스캔을 시작할 때 사용하는 ScanSettings에 달려 있습니다.
이전에는 다음과 같은 작업을 수행했을 수도 있습니다:
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를 이해해야 합니다. Ghostbusters의 PKE 미터라고 생각하십시오. 이는 우리 주변에 떠다니는 보이지 않는 에너지(우리의 경우 BLE 광고)를 감지하는 데 사용하는 도구입니다. 그런데 이 유령 사냥 도구는 실제로 어떻게 작동하나요?
건축:커튼 뒤 엿보기
높은 수준에서 프로세스는 매우 간단합니다. 자신만의 작은 세계에서 편안하게 생활하는 귀하의 앱은 BLE 장치를 찾기로 결정합니다. BluetoothLeScanner의 인스턴스를 잡고 "야, 가서 물건을 찾아봐"라고 말합니다.
내부적으로는 많은 일이 일어나고 있습니다. BluetoothLeScanner는 Android Bluetooth 스택(코드명 "Fluoride"로 치과 의사가 매우 자랑스러워할 것임)과 통신합니다. 그런 다음 스택은 전파 전송 및 수신을 수행하는 실제 하드웨어인 장치의 Bluetooth 컨트롤러와 통신합니다. "보이는 것보다 더 복잡하다"는 전형적인 사례입니다.
알파벳 수프:GATT, GAP 및 친구들
BLE의 세계에 뛰어들다 보면 수많은 약어들을 금세 접하게 될 것입니다. 당황하지 말 것! 그들은 보기만큼 무섭지 않습니다. 이해해야 할 가장 중요한 두 가지는 GAP와 GATT입니다.
-
GAP(일반 액세스 프로필): 이는 장치가 서로를 발견하고 연결하는 방법에 관한 것입니다. GAP를 나이트클럽의 경비원으로 생각해보세요. 누가 누구와 대화할지 결정합니다. 광고("나 여기 있어요!"라고 외치는 장치)와 검색(이 소리를 듣는 앱)을 관리합니다. BluetoothLeScanner는 GAP 분야의 핵심 플레이어입니다.
-
GATT(일반 속성 프로필): 두 장치가 연결되면 GATT가 대신합니다. 데이터를 교환하는 방법을 정의합니다. GATT를 나이트클럽 내부에서 일어나는 실제 대화라고 생각해보세요. 서비스, 특성 및 설명자에 관한 모든 것입니다. 장치에는 "심박수 측정 특성"이 포함된 "심박수 서비스"가 있을 수 있습니다. 앱은 필요한 데이터를 얻기 위해 이러한 특성을 읽거나 씁니다.
스캐닝을 위해 우리는 대부분 GAP의 세계에 살고 있습니다. 우리는 클럽 밖에 서서 재미있는 광고를 듣는 사람들입니다.
스캐닝 라이프사이클:3막의 극적인 연극
Bluetooth 스캔의 삶은 단순하면서도 우아한 드라마입니다.
-
1막: 준비. 앱에서 스캔할 시간을 결정합니다. BluetoothLeScanner를 가져오고, ScanFilters(특정 장치만 찾기 위해) 및 ScanSettings(새로운 수동 모드와 같이 스캔 방법을 정의하기 위해) 세트를 생성하고 ScanCallback을 정의합니다.
-
2막: 스캔. 앱이 startScan()을 호출합니다. Bluetooth 라디오가 생생하게 작동하여 필터와 일치하는 광고를 듣습니다. 하나를 찾으면 ScanCallback의 onScanResult() 메서드를 통해 앱에 다시 보고합니다.
-
3막: 끝. 앱이 충분하면(또는 더 중요한 것은 원하는 것을 찾았을 때) stopScan()을 호출합니다. 라디오의 전원이 꺼지고 모든 것이 다시 한 번 조용해졌습니다. 스캔이 끝나면 항상 스캔을 중지하는 것이 중요합니다. 악성 스캔은 사용자가 "한 시간 안에 배터리가 방전됩니다"라는 불만을 제기하는 가장 큰 원인입니다.
간단히 말해서 이것이 BluetoothLeScanner입니다. 이는 BLE 발견의 세계로 향하는 관문입니다. 강력하고 복잡하지만, 우리가 배워가면서 Android가 새로 출시될 때마다 점점 더 스마트해지고 효율적이 되고 있습니다. 이제 도구를 알았으니 손을 더럽혀 첫 번째 패시브 스캐너를 만들어 보겠습니다!
실습:첫 번째 패시브 스캐너 구축
이론은 훌륭하지만 솔직히 말해서 우리는 개발자입니다. 우리는 수행(또는 스택 오버플로에서 붙여넣기)을 통해 학습합니다. 이제 소매를 걷어붙이고 Android Studio를 실행하여 무언가를 구축할 시간입니다. 우리는 새로 발견된 수동 검색 기능을 사용하여 근처의 BLE 장치를 찾는 간단한 앱을 만들 것입니다.
1단계:허가 조사
Kotlin의 한 줄을 작성하기 전에 Android 권한 신을 달래야 합니다. 이것은 신성하고 종종 좌절감을 주는 의식입니다. 블루투스 검색의 경우 규칙은 수년에 걸쳐 약간 변경되었습니다.
먼저 AndroidManifest.xml을 엽니다. 그리고 다음을 추가하세요:
<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" />
위에서 선언한 권한을 살펴보면 Android 블루투스 권한 모델의 진화가 실시간으로 진행되는 것을 볼 수 있습니다.
처음 두 권한, BLUETOOTH 그리고 BLUETOOTH_ADMIN , 늙은 경비원입니다. Android 초기부터 사용되어 왔으며 기본적인 Bluetooth 기능과 장치 검색 기능을 제공합니다. 그러면 BLUETOOTH_SCAN가 있습니다. 는 Android 12(API 31)에 도입되었으며 개인정보 보호에 대한 Google의 생각에 큰 변화를 가져왔습니다.
네, 그렇게 보시는군요. 옛날(Android 12 이전)에 Google은 블루투스 기기를 찾는 것이 기본적으로 사용자의 정확한 위치를 아는 것과 같다고 결정했습니다. 그것은 일종의 의미가 있습니다. 결국 어떤 Bluetooth 비콘이 근처에 있는지 알 수 있다면 위치를 삼각 측량할 수 있습니다. 하지만 단지 헤드폰을 찾기 위해 위치를 묻는 것도 조금 오싹했습니다. 이로 인해 사용자가 자신의 정확한 위치를 묻는 간단한 블루투스 스캐너 앱을 보고 당연히 의심하게 되는 어색한 상황이 발생했습니다.
다행히 Android 12에서는 BLUETOOTH_SCAN을 도입했습니다. 허가를 받는 것이 훨씬 더 합리적입니다. 이 권한을 통해 마침내 앱은 위치 액세스를 요청할 필요 없이 Bluetooth 장치를 검색할 수 있게 되었으며, 이는 사용자 관점에서 훨씬 더 의미가 있습니다. 런타임 시 이 권한을 요청해야 하지만 최소한 간단한 가젯 찾기 앱이 사용자의 거주지를 알고 싶어하는 이유를 사용자에게 설명할 필요는 없습니다.
그러나 위치 액세스에 대한 마지막 두 가지 권한을 확인하세요. 그것들은 낡은 시스템의 잔재들이다. Android 11 이하를 실행하는 이전 기기를 지원해야 하는 앱을 빌드하는 경우 이전 버전과의 호환성을 위해 매니페스트에 이러한 위치 권한을 유지해야 합니다. 최신 기기에서는 BLUETOOTH_SCAN 허가만으로도 작업이 완료됩니다.
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
}
위의 코드에서 무슨 일이 일어나고 있는지 분석해 보겠습니다. 우리는 Kotlin의 lazy을 사용하고 있습니다. 위임은 "실제로 필요할 때까지 이 개체를 만들지 마세요"라고 말하는 멋진 방법입니다. Bluetooth 어댑터를 얻으려면 시스템 호출이 필요하고 실제로 사용하지 않으면 해당 작업을 수행할 필요가 없기 때문에 이는 좋은 습관입니다.
먼저 BluetoothManager을 가져옵니다. 시스템 서비스에서. BluetoothManager을 생각해 보세요 장치의 Bluetooth에 관한 모든 것에 대한 문지기 역할을 합니다. 이 관리자로부터 BluetoothAdapter를 얻습니다. 은 기기의 실제 Bluetooth 하드웨어를 나타냅니다. nullable(BluetoothAdapter?)로 선언하고 있다는 점에 유의하세요. ) 믿거나 말거나 모든 Android 기기에 Bluetooth가 있는 것은 아니기 때문입니다. 일부 태블릿이나 잘 알려지지 않은 장치에는 하드웨어가 없을 수도 있으므로 그러한 가능성에 대비해야 합니다.
어댑터가 있으면 BluetoothLeScanner를 요청할 수 있습니다. . 이것이 스캔을 수행하는 데 사용할 실제 개체입니다. 이번에도 안전 통화 연산자(?.)를 사용하고 있습니다. ) 왜냐하면 어댑터가 null(Bluetooth 하드웨어 없음)이면 스캐너를 얻을 수 없기 때문입니다. 이러한 방어 프로그래밍은 편집증적인 것처럼 보일 수 있지만, 이는 알 수 없는 충돌을 일으키는 앱과 극단적인 경우를 우아하게 처리하는 앱을 구분하는 요소입니다.
콜백 정의
이것이 바로 마법이 일어나는 곳입니다. 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")
}
}
ScanCallback 위에서 정의한 것은 Bluetooth 세계에서 앱의 귀입니다. 스캐너가 장치를 찾으면 정보를 어딘가에 저장하는 것이 아니라 이 콜백 개체를 통해 앱을 적극적으로 호출합니다. 이는 고전적인 이벤트 중심 프로그래밍이며 Android가 메인 스레드를 차단하지 않고 앱의 응답성을 유지하는 방법입니다.
onScanResult 메서드는 스캐너가 필터와 일치하는 장치(또는 필터를 사용하지 않는 경우 모든 장치)를 발견할 때마다 호출됩니다. result 매개변수는 정보의 보고입니다. BluetoothDevice가 포함되어 있습니다. 개체(기기의 MAC 주소와 이름이 있음), RSSI 값(수신 신호 강도 표시기 – 기본적으로 기기가 얼마나 가까운지, 숫자가 높을수록 더 가깝다는 의미), 기기가 방송하는 원시 광고 데이터입니다.
위의 간단한 예에서는 MAC 주소와 RSSI만 기록하지만 실제 앱에서는 UI를 업데이트하거나, 장치를 목록에 추가하거나, 연결을 실행해야 할 수도 있습니다.
callbackType 매개변수는 이유를 알려줍니다. 이 콜백이 트리거되었습니다. CALLBACK_TYPE_ALL_MATCHES일 수 있습니다. (기본값은 "여기에 우리가 찾은 모든 장치가 있습니다"를 의미), CALLBACK_TYPE_FIRST_MATCH (이 장치를 처음 봤을 때) 또는 CALLBACK_TYPE_MATCH_LOST (우리는 한동안 이 장치를 보지 못했기 때문에 아마도 떠났을 것입니다). 고급 섹션에서 이러한 유형에 대해 자세히 알아보겠습니다.
그러면 onScanFailed가 있습니다 , 우리 모두가 호출되지 않기를 바라지만 반드시 처리해야 하는 메서드입니다. 이는 스캔에 심각한 문제가 발생할 때 호출됩니다. 스캔 도중에 Bluetooth 어댑터가 꺼졌을 수도 있고, 앱에 올바른 권한이 없을 수도 있고, Bluetooth 컨트롤러에 문제가 생겼을 수도 있습니다. errorCode 그러면 무엇이 잘못되었는지에 대한 힌트가 제공되므로 항상 이를 기록하고 적절하게 처리해야 합니다. 예를 들어 사용자에게 메시지를 표시하거나 잠시 후 검색을 다시 시작하는 등의 방법을 사용할 수 있습니다.
스캔 구성
이제 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()
ScanSettings 위에서 우리가 만들고 있는 객체는 블루투스 스캐너에 대한 자세한 사용 설명서와 같습니다. 각 메소드 호출은 스캔이 어떻게 작동해야 하는지 정확하게 미세 조정하며, 이러한 설정을 올바르게 설정하는 것이 배터리 친화적인 앱과 몇 시간 내에 제거되는 앱의 차이입니다.
각 설정을 살펴보겠습니다. 먼저, setScanMode(SCAN_MODE_LOW_POWER) 스캐너에 저전력 스캔 모드를 사용하도록 지시합니다. 즉, 연속이 아닌 간격을 두고 스캔합니다. 이는 즉각적인 결과가 필요하지 않고 배터리 수명을 보존하려는 대부분의 사용 사례에 적합합니다. 스캐너가 깨어나서 잠시 스캔하고 잠자기를 반복합니다. 이는 낮잠을 자는 것과 같은 블루투스 기능입니다.
다음은 setCallbackType(CALLBACK_TYPE_ALL_MATCHES) 스캐너가 일치하는 장치를 찾을 때마다 알림을 받고 싶다는 의미입니다. 이는 기본 동작이며 대부분의 경우 사용됩니다. 앞서 언급했듯이 CALLBACK_TYPE_FIRST_MATCH를 사용할 수도 있습니다. 또는 CALLBACK_TYPE_MATCH_LOST 더욱 정교한 존재 감지가 가능합니다.
setMatchMode(MATCH_MODE_AGGRESSIVE) 설정은 하드웨어가 필터에 대해 장치를 얼마나 적극적으로 일치시키려고 시도해야 하는지를 제어합니다. MATCH_MODE_AGGRESSIVE 100% 확실하지 않더라도 일치 항목을 빠르게 보고함을 의미합니다. MATCH_MODE_STICKY "보고하기 전에 정말 확신할 때까지 기다리세요"를 의미합니다. 공격적 모드를 사용하면 더 빠른 결과를 얻을 수 있지만 때때로 오탐지가 발생할 수도 있습니다.
그러면 setNumOfMatches(MATCH_NUM_ONE_ADVERTISEMENT)이 있습니다. , 이는 장치에서 단 하나의 광고만 본 후 장치를 보고하도록 스캐너에 지시합니다. 대안은 MATCH_NUM_FEW_ADVERTISEMENT입니다. , 보고하기 전에 여러 광고를 기다립니다. 하나의 광고를 사용하면 더 빠르게 발견할 수 있으며, 몇 가지 광고를 기다리는 동안 방금 지나가는 장치의 오탐이 줄어듭니다.
setReportDelay(0L) 설정이 중요합니다. 0 지연 "결과를 즉시 보고한다"는 의미입니다. 예를 들어 5000으로 설정하면 밀리초 단위로 스캐너는 결과를 일괄 처리하고 5초마다 제공합니다. 일괄 처리는 고급 섹션에서 논의한 것처럼 백그라운드 검색에 적합하지만 사용자가 적극적으로 기다리고 있는 포그라운드 검색의 경우 즉각적인 보고가 필요합니다.
마지막으로 우리 쇼의 주인공은 setScanType(SCAN_TYPE_PASSIVE)입니다. . 이것은 스캐너를 자동 청취자로 변환하는 Android 16 QPR2의 새로운 API입니다. 듣는 모든 장치에 적극적으로 스캔 요청을 보내는 대신, 공중에 떠다니는 광고만 듣습니다. 이 단일 설정으로 스캔 중에 앱의 배터리 소모를 크게 줄일 수 있습니다. 이는 우리가 기다려 왔던 기능이며 정말 영광입니다.
검사 시작 및 중지
마지막으로 스캔을 시작하고 중지하는 기능이 필요합니다. 기억하세요:항상 스캔을 중지하세요! 잊혀진 스캔은 배터리를 소모하는 괴물입니다.
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.")
}
}
위의 두 가지 기능은 Bluetooth 스캐너의 켜기/끄기 스위치이며, 그 중요성에 비해 믿을 수 없을 정도로 간단합니다. 각각의 상황을 분석해 보겠습니다.
startBleScan()에서 , 먼저 bleScanner가 있는지 확인합니다. null이 아닙니다. 이것이 우리의 안전망입니다. 장치에 Bluetooth 하드웨어가 없거나 Bluetooth가 비활성화된 경우 스캐너는 null이 되며 우리는 null 개체에 대한 메서드 호출을 시도하여 충돌이 발생하는 것을 원하지 않습니다. 스캐너가 존재하면 startScan()를 호출합니다. 세 개의 매개변수 포함:ScanFilter 목록 객체, 우리가 세심하게 제작한 ScanSettings 및 ScanCallback 앞서 정의했습니다.
scanFilters 이 예에서는 목록이 현재 비어 있습니다. 이는 "모든 BLE 장치 찾기"를 의미합니다. 실제 앱에서는 일반적으로 검색 범위를 좁히기 위해 여기에 필터를 추가합니다.
예를 들어 심박수 모니터에서만 작동하는 앱을 구축하는 경우 심박수 서비스 UUID를 광고하는 장치만 일치시키는 필터를 생성합니다. 이는 성능과 배터리 수명 모두에 매우 중요합니다. 피트니스 트래커에만 관심이 있는데 왜 무작위로 Bluetooth 칫솔이 나올 때마다 앱을 깨우나요?
startScan() 방법은 스캔 프로세스를 시작합니다. 이 시점부터 Bluetooth 라디오는 광고를 적극적으로(또는 우리의 경우 수동적으로) 수신하며 scanCallback 결과를 받기 시작합니다. 이는 비동기 작업입니다. 즉, 코드가 결과를 기다리면서 여기에서 차단되지 않고 계속 실행되며 결과는 가능할 때마다 콜백을 통해 수신됩니다.
이제 stopBleScan()에 대해 이야기해 보겠습니다. , 이는 여러분이 작성하는 가장 중요한 기능일 수 있습니다. stopScan()에 전화하면 콜백을 통해 Bluetooth 라디오에 "좋아, 끝났습니다. 다시 잠자기 상태로 돌아가셔도 됩니다."라고 말하는 것입니다. 그러면 검색 프로세스가 즉시 중지되고 리소스가 해제됩니다.
이해해야 할 중요한 점은 이를 호출하지 않으면 스캔이 무한정 계속 실행되어 무제한 혈액 은행의 뱀파이어처럼 사용자의 배터리를 소모한다는 것입니다. 이것이 바로 우리가 잊혀진 stopScan()을 강조하는 이유입니다. 통화는 블루투스 앱에서 배터리 소모 불만을 일으키는 가장 일반적인 원인 중 하나입니다.
동일한 scanCallback를 전달하고 있다는 점에 유의하세요. stopScan()에 반대 startScan()에서 사용한 것 . 이것이 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 스캐너입니다. 여기서 사용하고 있는 몇 가지 중요한 패턴을 강조해 보겠습니다.
먼저, 버튼 클릭을 통해 스캔 수명주기를 사용자 작업과 어떻게 연결하는지 살펴보세요. 이는 일반적인 패턴입니다. 사용자가 명시적으로 검색을 시작하고 중지하여 앱이 Bluetooth를 사용할 때 제어권을 부여합니다. 사용자가 원할 때만 스캔이 실행되므로 이는 좋은 UX와 배터리 수명에도 좋습니다.
하지만 정말 중요한 부분은 onPause()입니다. 재정의. 이것은 중요한 안전망입니다. 활동이 백그라운드로 전환되면(사용자가 홈 버튼을 눌렀거나 다른 앱으로 전환했을 수 있음) onPause() 호출되면 즉시 스캔을 중지합니다. 사용자가 앱을 볼 수 없는 경우 검색 결과가 필요하지 않고 배터리를 소모할 이유가 없기 때문에 이는 필수적입니다. 이 패턴을 사용하면 사용자가 "중지" 버튼을 누르는 것을 잊어버린 경우에도 검사가 백그라운드에서 영원히 실행되지 않습니다.
"onResume()는 어떻습니까?"라고 궁금해하실 수도 있습니다. ? 사용자가 돌아오면 검사를 다시 시작해야 하지 않나요?" 그것은 디자인 결정입니다. 일부 앱에서는 onResume()에서 자동으로 검사를 다시 시작하고 싶을 수도 있습니다. . 다른 경우에는 사용자가 명시적으로 "시작"을 다시 누르도록 할 수도 있습니다. 사용 사례에 따라 다릅니다. 사용자가 적극적으로 검색하는 기기 찾기 앱의 경우 자동 재개가 적합합니다. 백그라운드에서 실행되는 모니터링 앱의 경우 보다 명시적인 제어가 필요할 수 있습니다.
이 예제에서 보여주지 않은 중요한 것 중 하나는 런타임 권한 처리입니다. 매니페스트에서 선언한 권한을 기억하시나요? Android 6.0 이상에서는 선언만 할 수 없으며 실제로 런타임 시 사용자에게 요청해야 합니다. startBleScan()에 전화하기 전에 , 필요한 권한이 있는지 확인하고, 없으면 ActivityCompat.requestPermissions()를 사용하여 요청해야 합니다. . 적절한 권한 없이 검사를 시작하려고 하면 자동으로(또는 Android 버전에 따라 큰 소리로) 실패하며 왜 아무 것도 작동하지 않는지 궁금해 머리를 긁적일 것입니다.
그리고 거기에 있습니다! 첫 번째 AOSP 16 패시브 Bluetooth 스캐너를 만들었습니다. 얇고 비열하며 믿을 수 없을 만큼 전력 효율적입니다. 스캐너는 BLE 광고를 조용히 듣고 콜백을 통해 보고하며 필요하지 않을 때는 정상적으로 중지됩니다.
이제 다음 주제인 문제가 발생했을 때 어떻게 해야 하는지로 넘어가겠습니다. 이제 이별에 대해 이야기할 시간입니다... 블루투스 결합 이별, 즉.
심층 분석 #2:블루투스 본드 손실 이유
아, 블루투스 본드. 그것은 아름답고 신성한 일입니다. 이는 우정 팔찌를 교환하는 것과 같은 디지털 방식입니다. 휴대폰과 헤드폰을 결합하면 장기적이며 신뢰할 수 있는 관계가 형성됩니다. 그들은 비밀 키를 공유하고, 서로를 기억하며, 자동으로 연결될 것을 약속하므로 매번 페어링해야 하는 번거로움이 줄어듭니다. 아름다운 로맨스입니다.
그렇지 않을 때까지.
어느 날 갑자기 그들은... 서로를 잊어버리게 됩니다. 연결이 끊어졌습니다. 신뢰가 깨졌습니다. 그리고 앱은 무엇이 잘못되었는지 전혀 모르고 치료사 역할을 하려고 중간에 남아 있습니다. 당신은 유령이 되었습니다. 그리고 지금까지 안드로이드는 아무런 도움이 되지 못했습니다. 이제 결합 상태가 BOND_NONE이라는 알림을 받게 되지만 그게 전부입니다. 설명이 없습니다. 폐쇄 없음. 연결 실패로 인한 차갑고 딱딱한 침묵뿐이었습니다.
마침내 마무리!
하지만 Android 팀의 친구들은 분명 힘든 이별을 겪었습니다. AOSP 16에서 그들은 우리에게 종결이라는 선물을 줬기 때문입니다. BluetoothDevice.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
본드 손실의 원인이 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:
-
Scan:Find the device.
-
Connect:Establish a connection (a slow and power-hungry process).
-
Discover Services:Ask the device, "So... what do you do for a living?" and wait for it to list all its services.
-
Evaluate:Check if the list of services contains the one you're interested in.
-
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 재산. 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 객체. 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. 이것이 바로 마법이 일어나는 곳입니다. 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.
경고: 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 이벤트. 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 이벤트. 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
We've all been there. 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.
그럼 다음은 무엇입니까? 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명 이상의 사람들이 개발자로 취업하는 데 도움을 주었습니다. 시작하세요