summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/AppScanStats.java14
-rw-r--r--android/pandora/test/main.py2
-rw-r--r--android/pandora/test/rfcomm_test.py110
-rw-r--r--flags/sockets.aconfig10
-rw-r--r--offload/leaudio/hci/proxy.rs30
-rw-r--r--system/gd/hci/distance_measurement_manager_test.cc2
-rw-r--r--system/pdl/hci/hci_packets.pdl4
-rw-r--r--system/stack/rfcomm/port_api.cc4
-rw-r--r--system/stack/rfcomm/rfc_l2cap_if.cc54
-rw-r--r--system/test/README.md81
10 files changed, 190 insertions, 121 deletions
diff --git a/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java b/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
index 97e1f5ede5..b5090eabd7 100644
--- a/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
+++ b/android/app/src/com/android/bluetooth/le_scan/AppScanStats.java
@@ -195,6 +195,7 @@ class AppScanStats {
mTimeProvider = requireNonNull(timeProvider);
}
+ @Nullable
private synchronized LastScan getScanFromScannerId(int scannerId) {
return mOngoingScans.get(scannerId);
}
@@ -530,7 +531,7 @@ class AppScanStats {
convertScanType(getScanFromScannerId(scannerId)),
BluetoothStatsLog.LE_SCAN_ABUSED__LE_SCAN_ABUSE_REASON__REASON_SCAN_TIMEOUT,
scanTimeoutMillis,
- getScanFromScannerId(scannerId).getAttributionTag());
+ getAttributionTagFromScannerId(scannerId));
}
MetricsLogger.getInstance()
.cacheCount(BluetoothProtoEnums.LE_SCAN_ABUSE_COUNT_SCAN_TIMEOUT, 1);
@@ -546,7 +547,7 @@ class AppScanStats {
convertScanType(getScanFromScannerId(scannerId)),
BluetoothStatsLog.LE_SCAN_ABUSED__LE_SCAN_ABUSE_REASON__REASON_HW_FILTER_NA,
numOfFilterSupported,
- getScanFromScannerId(scannerId).getAttributionTag());
+ getAttributionTagFromScannerId(scannerId));
}
MetricsLogger.getInstance()
.cacheCount(BluetoothProtoEnums.LE_SCAN_ABUSE_COUNT_HW_FILTER_NOT_AVAILABLE, 1);
@@ -563,7 +564,7 @@ class AppScanStats {
BluetoothStatsLog
.LE_SCAN_ABUSED__LE_SCAN_ABUSE_REASON__REASON_TRACKING_HW_FILTER_NA,
numOfTrackableAdv,
- getScanFromScannerId(scannerId).getAttributionTag());
+ getAttributionTagFromScannerId(scannerId));
}
MetricsLogger.getInstance()
.cacheCount(
@@ -596,7 +597,7 @@ class AppScanStats {
sRadioScanIntervalMs = scanIntervalMs;
sIsRadioStarted = true;
sRadioScanAppImportance = stats.mAppImportance;
- sRadioScanAttributionTag = stats.getScanFromScannerId(scannerId).getAttributionTag();
+ sRadioScanAttributionTag = stats.getAttributionTagFromScannerId(scannerId);
}
return true;
}
@@ -849,6 +850,11 @@ class AppScanStats {
< LARGE_SCAN_TIME_GAP_MS);
}
+ private String getAttributionTagFromScannerId(int scannerId) {
+ LastScan scan = getScanFromScannerId(scannerId);
+ return scan == null ? "" : scan.getAttributionTag();
+ }
+
private static String filterToStringWithoutNullParam(ScanFilter filter) {
StringBuilder filterString = new StringBuilder("BluetoothLeScanFilter [");
if (filter.getDeviceName() != null) {
diff --git a/android/pandora/test/main.py b/android/pandora/test/main.py
index d5c5da0bad..7e49321932 100644
--- a/android/pandora/test/main.py
+++ b/android/pandora/test/main.py
@@ -25,6 +25,7 @@ import avatar.cases.security_test
import gatt_test
import hap_test
import hfpclient_test
+import rfcomm_test
import sdp_test
from pairing import _test_class_list as _pairing_test_class_list
@@ -81,6 +82,7 @@ _TEST_CLASSES_LIST = [
hap_test.HapTest,
asha_test.AshaTest,
hfpclient_test.HfpClientTest,
+ rfcomm_test.RfcommTest,
] + _pairing_test_class_list
diff --git a/android/pandora/test/rfcomm_test.py b/android/pandora/test/rfcomm_test.py
new file mode 100644
index 0000000000..4409b02e25
--- /dev/null
+++ b/android/pandora/test/rfcomm_test.py
@@ -0,0 +1,110 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import asyncio
+import avatar
+import grpc
+import logging
+
+from avatar import PandoraDevices
+from avatar.aio import asynchronous
+from avatar.pandora_client import BumblePandoraClient, PandoraClient
+from bumble.rfcomm import Server
+from bumble_experimental.rfcomm import RFCOMMService
+from mobly import base_test, test_runner
+from mobly.asserts import assert_equal # type: ignore
+from mobly.asserts import assert_in # type: ignore
+from mobly.asserts import assert_is_not_none # type: ignore
+from mobly.asserts import fail # type: ignore
+from pandora_experimental.rfcomm_grpc_aio import RFCOMM
+from pandora_experimental.rfcomm_pb2 import (
+ AcceptConnectionRequest,
+ RxRequest,
+ StartServerRequest,
+ StopServerRequest,
+ TxRequest,
+)
+from typing import Optional, Tuple
+
+SERIAL_PORT_UUID = "00001101-0000-1000-8000-00805F9B34FB"
+TEST_SERVER_NAME = "RFCOMM-Server"
+
+
+class RfcommTest(base_test.BaseTestClass):
+ devices: Optional[PandoraDevices] = None
+ dut: PandoraClient
+ ref: BumblePandoraClient
+
+ def setup_class(self) -> None:
+ self.devices = PandoraDevices(self)
+ self.dut, ref, *_ = self.devices
+ assert isinstance(ref, BumblePandoraClient)
+ self.ref = ref
+ # Enable BR/EDR mode and SSP for Bumble devices.
+ self.ref.config.setdefault('classic_enabled', True)
+ self.ref.config.setdefault('classic_ssp_enabled', True)
+ self.ref.config.setdefault(
+ 'server',
+ {
+ 'io_capability': 'no_output_no_input',
+ },
+ )
+
+ def teardown_class(self) -> None:
+ if self.devices:
+ self.devices.stop_all()
+
+ @avatar.asynchronous
+ async def setup_test(self) -> None:
+ await asyncio.gather(self.dut.reset(), self.ref.reset())
+
+ ref_server = Server(self.ref.device)
+ self.ref.rfcomm = RFCOMMService(self.ref.device, ref_server)
+ self.dut.rfcomm = RFCOMM(channel=self.dut.aio.channel)
+
+ @avatar.asynchronous
+ async def test_client_connect_and_exchange_data(self) -> None:
+ # dut is client, ref is server
+ context = grpc.ServicerContext
+ server = await self.ref.rfcomm.StartServer(StartServerRequest(name=TEST_SERVER_NAME, uuid=SERIAL_PORT_UUID),
+ context=context)
+ # Convert StartServerResponse to its server
+ server = server.server
+ rfc_dut_ref, rfc_ref_dut = await asyncio.gather(
+ self.dut.rfcomm.ConnectToServer(address=self.ref.address, uuid=SERIAL_PORT_UUID),
+ self.ref.rfcomm.AcceptConnection(request=AcceptConnectionRequest(server=server), context=context))
+ # Convert Responses to their corresponding RfcommConnection
+ rfc_dut_ref = rfc_dut_ref.connection
+ rfc_ref_dut = rfc_ref_dut.connection
+
+ # Transmit data
+ tx_data = b'Data from dut to ref'
+ await self.dut.rfcomm.Send(data=tx_data, connection=rfc_dut_ref)
+ ref_receive = await self.ref.rfcomm.Receive(request=RxRequest(connection=rfc_ref_dut), context=context)
+ assert_equal(ref_receive.data, tx_data)
+
+ # Receive data
+ rx_data = b'Data from ref to dut'
+ await self.ref.rfcomm.Send(request=TxRequest(connection=rfc_ref_dut, data=rx_data), context=context)
+ dut_receive = await self.dut.rfcomm.Receive(connection=rfc_dut_ref)
+ assert_equal(dut_receive.data.rstrip(b'\x00'), rx_data)
+
+ # Disconnect (from dut)
+ await self.dut.rfcomm.Disconnect(connection=rfc_dut_ref)
+ await self.ref.rfcomm.StopServer(request=StopServerRequest(server=server), context=context)
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG)
+ test_runner.main() # type: ignore
diff --git a/flags/sockets.aconfig b/flags/sockets.aconfig
index 2eb23b1fcb..6b31e06705 100644
--- a/flags/sockets.aconfig
+++ b/flags/sockets.aconfig
@@ -92,3 +92,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "fix_lecoc_socket_available"
+ namespace: "bluetooth"
+ description: "Fix Bluetooth Socket available API for LECOC socket"
+ bug: "402536099"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/offload/leaudio/hci/proxy.rs b/offload/leaudio/hci/proxy.rs
index 933389d315..adc2dbd34d 100644
--- a/offload/leaudio/hci/proxy.rs
+++ b/offload/leaudio/hci/proxy.rs
@@ -195,10 +195,16 @@ impl Module for LeAudioModule {
);
}
- Ok(Command::LeSetupIsoDataPath(ref c)) if c.data_path_id == DATA_PATH_ID_SOFTWARE => {
+ Ok(Command::LeSetupIsoDataPath(ref c)) if c.data_path_id == DATA_PATH_ID_SOFTWARE => 'command: {
assert_eq!(c.data_path_direction, hci::LeDataPathDirection::Input);
let mut state = self.state.lock().unwrap();
- let stream = state.stream.get_mut(&c.connection_handle).unwrap();
+ let Some(stream) = state.stream.get_mut(&c.connection_handle) else {
+ log::warn!(
+ "Setup ISO Data Path on non existing BIS/CIS handle: 0x{:03x}",
+ c.connection_handle
+ );
+ break 'command;
+ };
stream.state = StreamState::Enabling;
// Phase 1 limitation: The controller does not implement HCI Link Feedback event,
@@ -209,6 +215,16 @@ impl Module for LeAudioModule {
return;
}
+ Ok(Command::LeRemoveIsoDataPath(ref c)) => {
+ let mut state = self.state.lock().unwrap();
+ if state.stream.get_mut(&c.connection_handle).is_none() {
+ log::warn!(
+ "Remove ISO Data Path on non existing BIS/CIS handle: 0x{:03x}",
+ c.connection_handle
+ );
+ }
+ }
+
_ => (),
}
@@ -250,7 +266,9 @@ impl Module for LeAudioModule {
ReturnParameters::LeSetupIsoDataPath(ref ret) => 'event: {
let mut state = self.state.lock().unwrap();
- let stream = state.stream.get_mut(&ret.connection_handle).unwrap();
+ let Some(stream) = state.stream.get_mut(&ret.connection_handle) else {
+ break 'event;
+ };
stream.state =
if stream.state == StreamState::Enabling && ret.status == Status::Success {
StreamState::Enabled
@@ -278,9 +296,11 @@ impl Module for LeAudioModule {
);
}
- ReturnParameters::LeRemoveIsoDataPath(ref ret) if ret.status == Status::Success => {
+ ReturnParameters::LeRemoveIsoDataPath(ref ret) if ret.status == Status::Success => 'event: {
let mut state = self.state.lock().unwrap();
- let stream = state.stream.get_mut(&ret.connection_handle).unwrap();
+ let Some(stream) = state.stream.get_mut(&ret.connection_handle) else {
+ break 'event;
+ };
if stream.state == StreamState::Enabled {
Service::stop_stream(ret.connection_handle);
}
diff --git a/system/gd/hci/distance_measurement_manager_test.cc b/system/gd/hci/distance_measurement_manager_test.cc
index d3b7706ad0..3f6e16ba99 100644
--- a/system/gd/hci/distance_measurement_manager_test.cc
+++ b/system/gd/hci/distance_measurement_manager_test.cc
@@ -78,7 +78,7 @@ struct CsReadCapabilitiesCompleteEvent {
CsOptionalNadmSoundingCapability nadm_sounding_capability = {
/*normalized_attack_detector_metric=*/1};
CsOptionalNadmRandomCapability nadm_random_capability = {/*normalized_attack_detector_metric=*/1};
- CsOptionalCsSyncPhysSupported cs_sync_phys_supported = {/*le_2m_phy=*/1};
+ CsOptionalCsSyncPhysSupported cs_sync_phys_supported = {/*le_2m_phy=*/1, /*le_2m_2bt_phy=*/0};
CsOptionalSubfeaturesSupported subfeatures_supported = {/*no_frequency_actuation_error=*/1,
/*channel_selection_algorithm=*/1,
/*phase_based_ranging=*/1};
diff --git a/system/pdl/hci/hci_packets.pdl b/system/pdl/hci/hci_packets.pdl
index fcdd255b99..838b1fa586 100644
--- a/system/pdl/hci/hci_packets.pdl
+++ b/system/pdl/hci/hci_packets.pdl
@@ -4949,7 +4949,8 @@ struct CsOptionalNadmRandomCapability {
struct CsOptionalCsSyncPhysSupported {
le_2m_phy : 1,
- _reserved_ : 7,
+ le_2m_2bt_phy : 1,
+ _reserved_ : 6,
}
struct CsOptionalSubfeaturesSupported {
@@ -5151,6 +5152,7 @@ enum CsConfigRttType : 8 {
enum CsSyncPhy : 8 {
LE_1M_PHY = 0x01,
LE_2M_PHY = 0x02,
+ LE_2M_2BT_PHY = 0x03,
}
enum CsChannelSelectionType : 8 {
diff --git a/system/stack/rfcomm/port_api.cc b/system/stack/rfcomm/port_api.cc
index a78f3ea890..6ab7cdccea 100644
--- a/system/stack/rfcomm/port_api.cc
+++ b/system/stack/rfcomm/port_api.cc
@@ -1229,7 +1229,9 @@ int PORT_GetChannelInfo(uint16_t handle, uint16_t* local_mtu, uint16_t* remote_m
return PORT_NOT_OPENED;
}
- if (p_port->line_status) {
+ if (p_port->rfc.p_mcb == nullptr || p_port->line_status) {
+ log::warn("PORT_LINE_ERR - p_port->rfc.p_mcb == nullptr:{} p_port->line_status:{}",
+ (p_port->rfc.p_mcb == nullptr) ? "T" : "F", p_port->line_status);
return PORT_LINE_ERR;
}
diff --git a/system/stack/rfcomm/rfc_l2cap_if.cc b/system/stack/rfcomm/rfc_l2cap_if.cc
index 5b7fc00185..10826d235a 100644
--- a/system/stack/rfcomm/rfc_l2cap_if.cc
+++ b/system/stack/rfcomm/rfc_l2cap_if.cc
@@ -91,28 +91,26 @@ void rfcomm_l2cap_if_init(void) {
void RFCOMM_ConnectInd(const RawAddress& bd_addr, uint16_t lcid, uint16_t /* psm */, uint8_t id) {
tRFC_MCB* p_mcb = rfc_alloc_multiplexer_channel(bd_addr, false);
- if ((p_mcb) && (p_mcb->state != RFC_MX_STATE_IDLE)) {
- /* if this is collision case */
- if ((p_mcb->is_initiator) && (p_mcb->state == RFC_MX_STATE_WAIT_CONN_CNF)) {
- p_mcb->pending_lcid = lcid;
-
- /* wait random timeout (2 - 12) to resolve collision */
- /* if peer gives up then local device rejects incoming connection and
- * continues as initiator */
- /* if timeout, local device disconnects outgoing connection and continues
- * as acceptor */
- log::verbose(
- "RFCOMM_ConnectInd start timer for collision, initiator's "
- "LCID(0x{:x}), acceptor's LCID(0x{:x})",
- p_mcb->lcid, p_mcb->pending_lcid);
-
- rfc_timer_start(p_mcb, (uint16_t)(bluetooth::common::time_get_os_boottime_ms() % 10 + 2));
- return;
- } else {
- /* we cannot accept connection request from peer at this state */
- /* don't update lcid */
- p_mcb = nullptr;
- }
+ if (p_mcb != nullptr && p_mcb->is_initiator && p_mcb->state == RFC_MX_STATE_WAIT_CONN_CNF) {
+ p_mcb->pending_lcid = lcid;
+
+ /* wait random timeout (2 - 12) to resolve collision */
+ /* if peer gives up then local device rejects incoming connection and
+ * continues as initiator */
+ /* if timeout, local device disconnects outgoing connection and continues
+ * as acceptor */
+ log::verbose(
+ "RFCOMM_ConnectInd start timer for collision, initiator's "
+ "LCID(0x{:x}), acceptor's LCID(0x{:x})",
+ p_mcb->lcid, p_mcb->pending_lcid);
+
+ rfc_timer_start(p_mcb, (uint16_t)(bluetooth::common::time_get_os_boottime_ms() % 10 + 2));
+ return;
+ }
+ if (p_mcb != nullptr && p_mcb->is_initiator && p_mcb->state != RFC_MX_STATE_IDLE) {
+ /* we cannot accept connection request from peer at this state */
+ /* don't update lcid */
+ p_mcb = nullptr;
} else {
/* store mcb even if null */
rfc_save_lcid_mcb(p_mcb, lcid);
@@ -141,7 +139,7 @@ void RFCOMM_ConnectInd(const RawAddress& bd_addr, uint16_t lcid, uint16_t /* psm
void RFCOMM_ConnectCnf(uint16_t lcid, tL2CAP_CONN result) {
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::error("RFCOMM_ConnectCnf LCID:0x{:x}", lcid);
return;
}
@@ -188,7 +186,7 @@ void RFCOMM_ConfigInd(uint16_t lcid, tL2CAP_CFG_INFO* p_cfg) {
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::error("RFCOMM_ConfigInd LCID:0x{:x}", lcid);
for (auto& [cid, mcb] : rfc_lcid_mcb) {
if (mcb != nullptr && mcb->pending_lcid == lcid) {
@@ -218,7 +216,7 @@ void RFCOMM_ConfigCnf(uint16_t lcid, uint16_t /* initiator */, tL2CAP_CFG_INFO*
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::error("RFCOMM_ConfigCnf no MCB LCID:0x{:x}", lcid);
return;
}
@@ -237,7 +235,7 @@ void RFCOMM_ConfigCnf(uint16_t lcid, uint16_t /* initiator */, tL2CAP_CFG_INFO*
void RFCOMM_DisconnectInd(uint16_t lcid, bool is_conf_needed) {
log::verbose("lcid:0x{:x}, is_conf_needed:{}", lcid, is_conf_needed);
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::warn("no mcb for lcid 0x{:x}", lcid);
return;
}
@@ -257,7 +255,7 @@ void RFCOMM_DisconnectInd(uint16_t lcid, bool is_conf_needed) {
void RFCOMM_BufDataInd(uint16_t lcid, BT_HDR* p_buf) {
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::warn("Cannot find RFCOMM multiplexer for lcid 0x{:x}", lcid);
osi_free(p_buf);
return;
@@ -351,7 +349,7 @@ void RFCOMM_BufDataInd(uint16_t lcid, BT_HDR* p_buf) {
void RFCOMM_CongestionStatusInd(uint16_t lcid, bool is_congested) {
tRFC_MCB* p_mcb = rfc_find_lcid_mcb(lcid);
- if (!p_mcb) {
+ if (p_mcb == nullptr) {
log::error("RFCOMM_CongestionStatusInd dropped LCID:0x{:x}", lcid);
return;
} else {
diff --git a/system/test/README.md b/system/test/README.md
deleted file mode 100644
index 1f43e952ef..0000000000
--- a/system/test/README.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Fluoride Bluetooth Tests
-
-This document refers to the tests in the packages/modules/Bluetooth/system/test directory.
-
-The tests are designed to be run when the Android runtime is not running. From a terminal, run:
-
-## Before you run tests
-```sh
-adb shell stop
-```
-
-## After you're done
-```sh
-adb shell start
-```
-
-## Running tests
-Then see what options the test script provides:
-
-```sh
-./run_unit_tests.sh --help
-```
-
-But for the impatient, run specific groups of tests like this:
-
-```sh
-./run_unit_tests.sh net_test_bluetooth
-```
-
-a single test:
-
-```sh
-./run_unit_tests.sh net_test_bluetooth.BluetoothTest.AdapterRepeatedEnableDisable
-```
-
-## Sample Output
-
-packages/modules/Bluetooth/system/test$ ./run_unit_tests.sh net_test_bluetooth
---- net_test_bluetooth ---
-pushing...
-/tbd/aosp-master/out/target/product/bullhead/data/nativetest/n...st_bluetooth: 1 file pushed. 9.2 MB/s (211832 bytes in 0.022s)
-running...
-
-Running main() from gtest_main.cc
-[==========] Running 11 tests from 2 test cases.
-[----------] Global test environment set-up.
-[----------] 6 tests from BluetoothTest
-[ RUN ] BluetoothTest.AdapterEnableDisable
-[ OK ] BluetoothTest.AdapterEnableDisable (2538 ms)
-[ RUN ] BluetoothTest.AdapterRepeatedEnableDisable
-[ OK ] BluetoothTest.AdapterRepeatedEnableDisable (11384 ms)
-[ RUN ] BluetoothTest.AdapterSetGetName
-[ OK ] BluetoothTest.AdapterSetGetName (2378 ms)
-[ RUN ] BluetoothTest.AdapterStartDiscovery
-[ OK ] BluetoothTest.AdapterStartDiscovery (2397 ms)
-[ RUN ] BluetoothTest.AdapterCancelDiscovery
-[ OK ] BluetoothTest.AdapterCancelDiscovery (2401 ms)
-[ RUN ] BluetoothTest.AdapterDisableDuringBonding
-[ OK ] BluetoothTest.AdapterDisableDuringBonding (11689 ms)
-[----------] 6 tests from BluetoothTest (32789 ms total)
-
-[----------] 5 tests from GattTest
-[ RUN ] GattTest.GattClientRegister
-[ OK ] GattTest.GattClientRegister (2370 ms)
-[ RUN ] GattTest.GattClientScanRemoteDevice
-[ OK ] GattTest.GattClientScanRemoteDevice (2273 ms)
-[ RUN ] GattTest.GattClientAdvertise
-[ OK ] GattTest.GattClientAdvertise (2236 ms)
-[ RUN ] GattTest.GattServerRegister
-[ OK ] GattTest.GattServerRegister (2391 ms)
-[ RUN ] GattTest.GattServerBuild
-[ OK ] GattTest.GattServerBuild (2435 ms)
-[----------] 5 tests from GattTest (11706 ms total)
-
-[----------] Global test environment tear-down
-[==========] 11 tests from 2 test cases ran. (44495 ms total)
-[ PASSED ] 11 tests.
-
-## Troubleshooting: Your phone is bricked!
-Probably not. See [After you're done](#After-you're-done)
-