BLEモジュールのスキャンプログラムを作成します。

  • Android携帯:SH-52E Android14(API34) bleバージョン5.3
  • BLEモジュール:XIAO ESP32C3 bleバージョン5.0
  • BLEモジュール:CC2541 SensorTag bleバージョン4.0
  • Android開発環境:Android Studio Otter 2 Feature Drop | 2025.2.2

BLEモジュールのスキャンプログラムの作成

Android携帯を使ってCC2541 SensorTagとの通信」の「Android携帯を使ってBLEモジュールのスキャン(その2)」で作成しました。

今回は、javaコードを見直し、Android Studio Otter 2で開発します。

  • Android 12 以上(API 31+)では BLUETOOTH_SCAN / BLUETOOTH_CONNECT を実行時に要求する必要があります(。
  • Manifest の BLUETOOTH_SCAN に android:usesPermissionFlags=”neverForLocation” を付けています。これはアプリが位置情報目的でスキャンを使用しない場合に、位置情報権限を不要にするために使います。

BleScan2\app\src\main\AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.tomosoft.BleScan2">
    <!-- Bluetooth 権限: Android 12(API 31) 以降は細分化された権限を使う -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!-- 端末に BLE が必要なら明示(任意) -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.BleScan2">
        <activity
            android:name="com.tomosoft.BleScan2.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

BleScan2\app\src\main\java\com\tomosoft\BleScan2\MainActivity.java

package com.tomosoft.BleScan2;

import android.os.Bundle;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_ENABLE_BT = 1;
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothLeScanner bleScanner;
    private final List<String> devices = new ArrayList<>();
    private ArrayAdapter<String> adapter;
    private TextView tvStatus;
    private ActivityResultLauncher<Intent> enableBtIntent;
    private final ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            if (ActivityCompat.checkSelfPermission( MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                // TODO: Consider calling
                //    ActivityCompat#requestPermissions
                // here to request the missing permissions, and then overriding
                //   public void onRequestPermissionsResult(int requestCode, String[] permissions,
                //                                          int[] grantResults)
                // to handle the case where the user grants the permission. See the documentation
                // for ActivityCompat#requestPermissions for more details.
                return;
            }
            String line = result.getDevice().getAddress() + " / " +
                    (result.getDevice().getName() != null ? result.getDevice().getName() : "N/A") +
                    " / RSSI:" + result.getRssi();
            if (!devices.contains(line)) {
                devices.add(0, line);
                adapter.notifyDataSetChanged();
            }
        }
    };

    private final String[] REQUIRED_PERMISSIONS_API31 = new String[]{
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_CONNECT
    };

    private final ActivityResultLauncher<String[]> requestPermissionLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
                boolean granted = true;
                for (Boolean b : result.values()) {
                    if (b == null || !b) { granted = false; break; }
                }
                if (granted) {
                    startBleScan();
                } else {
                    tvStatus.setText("Permissions denied");
                }
            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.root), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    ///////////////////
        Button btnStart = findViewById(R.id.btnStart);
        Button btnStop = findViewById(R.id.btnStop);
        tvStatus = findViewById(R.id.tvStatus);
        ListView listView = findViewById(R.id.listView);
        adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, devices);
        listView.setAdapter(adapter);

        BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = manager.getAdapter();

        btnStart.setOnClickListener(v -> prepareAndStartScan());
        btnStop.setOnClickListener(v -> stopBleScan());

    }
    private void prepareAndStartScan() {
        if (bluetoothAdapter == null) {
            tvStatus.setText("Bluetooth not supported");
            return;
        }
        if (!bluetoothAdapter.isEnabled()) {
            // リクエストして Bluetooth を有効化
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            //startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
            this.enableBtIntent.launch(enableBtIntent);
            return;
        }
        // Android 12+ の権限チェック
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            boolean scanGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED;
            boolean connectGranted = ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
            if (!scanGranted || !connectGranted) {
                requestPermissionLauncher.launch(REQUIRED_PERMISSIONS_API31);
                return;
            }
        } else {
            // Android 6〜11 の場合は LOCATION 権限が必要なことがある(スキャンで位置情報を推測できるため)
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1001);
                return;
            }
        }
        startBleScan();
    }

    private void startBleScan() {
        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
            tvStatus.setText("Bluetooth disabled");
            return;
        }
        // BluetoothLeScanner の取得(CONNECT 権限が必要な操作もある)
        bleScanner = bluetoothAdapter.getBluetoothLeScanner();
        if (bleScanner == null) {
            tvStatus.setText("BLE scanner not available");
            return;
        }
        devices.clear();
        adapter.notifyDataSetChanged();
        // 権限チェック(安全のため)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
                ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
            tvStatus.setText("BLUETOOTH_SCAN missing");
            return;
        }
        bleScanner.startScan(scanCallback);
        tvStatus.setText("Scanning...");
    }

    private void stopBleScan() {
        if (bleScanner != null) {
            // 権限チェッ���
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
                    ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                tvStatus.setText("BLUETOOTH_CONNECT missing");
                return;
            }
            try {
                bleScanner.stopScan(scanCallback);
            } catch (Exception e) {
                // まれに例外が出ることがあるのでキャッチ
                e.printStackTrace();
            }
            tvStatus.setText("Stopped");
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        stopBleScan();
    }

    // onActivityResult は startActivityForResult からの戻りを処理
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_ENABLE_BT) {
            if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
                tvStatus.setText("Bluetooth enabled");
                prepareAndStartScan();
            } else {
                tvStatus.setText("Bluetooth not enabled");
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }


}

BleScan2\app\src\main\res\layout\activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:orientation="vertical"
    android:padding="16dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btnStart"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Start Scan" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Stop Scan"
        android:layout_marginTop="8dp"/>

    <TextView
        android:id="@+id/tvStatus"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Status: idle"
        android:layout_marginTop="8dp"/>

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:dividerHeight="8dp"/>
</LinearLayout>

BLEモジュールのスキャンプログラムの実行

作成したスキャンプログラムを携帯にダウンロードして実行すると、次のような内容が画面に表示されます。