【Android】PCとBluetooth接続し、PCに文字列を送信する

# はじめに

AndroidスマホをPCとBluetooth接続し、PCに文字列を送信するアプリをBluetooth接続のサンプルコード[「android-BluetoothChat」](https://github.com/googlesamples/android-BluetoothChat)を基に作成しました。[「Android開発日記　AndroidのBluetooth通信（SPP）【初心者向け】」](https://mikulogi-tomo.hatenablog.com/entry/2018/01/04/033458)などにも同様の記事がありますが、将来の使い勝手を考慮し、接続先の選択を[android-BluetoothChat](https://github.com/googlesamples/android-BluetoothChat)と同様に別アクティビティでペア設定済みのデバイスから選択できたり、接続処理はUIスレッドとは別のスレッドでの処理としています。さらに、Android 12で動作させるために必要な変更を追加しています。

# 検証環境

この記事の内容は、以下の環境で検証した。
* Android Studio Chipmunk | 2021.2.1 Patch 1
* targetSdk 32
* 実機確認 Sony SOG04 Android 12,API 31

(2025年6月追記)
この記事の内容は、以下の環境で再検証し、サンプルとして提示しているソースコードを更新しました。
* Android Studio Meerkat Feature Drop | 2024.3.2 Patch 1
* targetSdk 34
* 実機確認 Sony SOG04 Android 14,API 34

# MainActivity.java

Android Studio の Empty Activity を新規プロジェクトで起こし、必要部分をコピペしていきたいと思います。

まず、`android-BluetoothChat` の `MainActivity` では bluetooth接続以降、Fragment で処理しているので、これを踏襲します。

つぎに、Bluetooth機器の操作にはRuntime Permissionが必要です。必要になってから許可しますか？と聞かれるより、アプリ開始直後にもらった方がいいような気がするので、`onCreate`から呼ばれるように追加しておきます。[「アプリの権限をリクエストする」](https://developer.android.com/training/permissions/requesting?hl=ja)などを参考にして追加しました。

```Java:MainActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentTransaction;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    /**
     * Tag for Log
     */
    private static final String TAG = "MainActivity";

    private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
            BluetoothChatFragment fragment = new BluetoothChatFragment();
            transaction.replace(R.id.main_layout, fragment);
            transaction.commit();
        }
        // Check if the user revoked runtime permissions.
        if (!checkPermissions()) {
            requestPermissions();
        }
    }
    /**
     * Return the current state of the permissions needed.
     */
    private boolean checkPermissions() {
        int permissionState = 0;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            permissionState = ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.BLUETOOTH_CONNECT);
        }
        Log.i(TAG, "android.os.Build.VERSION.SDK_INT = " + android.os.Build.VERSION.SDK_INT + ",checkPermissions = " + permissionState);    //  PERMISSION_GRANTED = 0
        return permissionState == PackageManager.PERMISSION_GRANTED;
    }

    private void requestPermissions() {
        boolean shouldProvideRationale = false;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            shouldProvideRationale = ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.BLUETOOTH_CONNECT);
        }

        // Provide an additional rationale to the user. This would happen if the user denied the
        // request previously, but didn't check the "Don't ask again" checkbox.
        if (shouldProvideRationale) {   // 今後は確認しない をチェックしているしているか判断する
            //  false ならば表示する必要が無いと判断でき、かつ「今後は確認しない」状態であると判断することができる
        } else {
            Log.i(TAG, "Requesting permission");
            // Request permission. It's possible this can be auto answered if device policy
            // sets the permission in a given state or the user denied the permission
            // previously and checked "Never ask again".
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.BLUETOOTH_CONNECT},
                        REQUEST_PERMISSIONS_REQUEST_CODE);
            }
        }
    }
}
```

レイアウト設定の方は、必要最小限にしておきます。


```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:id ="@+id/main_layout"
    >

</androidx.constraintlayout.widget.ConstraintLayout>
```

# BluetoothChatFragment.java

## Fragmentの追加

`MainActivity` から呼ばれるFragment を追加します。

`MainActivity`と同じ階層に`BluetoothChatFragment.java`を新規で作りますが、`BluetoothChat`の[`BluetoothChatFragment`](https://github.com/googlearchive/android-BluetoothChat/blob/d6e5db25764fe94f294eb1b6e9ea521dc2f02c71/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatFragment.java)は使わない部分もありそうなので、まずは `onCreate`と`onStart`、`onCreateView`をコピー。

```Java:BluetoothChatFragment.java
import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;

public class BluetoothChatFragment  extends Fragment {
    // Intent request codes
    private static final int REQUEST_ENABLE_BT = 3;
    /**
     * Local Bluetooth adapter
     */
    private BluetoothAdapter mBluetoothAdapter = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
        // Get local Bluetooth adapter
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        // If the adapter is null, then Bluetooth is not supported
        FragmentActivity activity = getActivity();
        if (mBluetoothAdapter == null && activity != null) {
            Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show();
            activity.finish();
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mBluetoothAdapter == null) {
            return;
        }
        // If BT is not on, request that it be enabled.
        // setupChat() will then be called during onActivityResult
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false);
    }
}
```

## 非推奨の`startActivityForResult()`に対応

あれっ、フラグメントの`startActivityForResult()` が非推奨になってる！ということで調べると[「アクティビティの結果を取得する」](https://developer.android.com/training/basics/intents/result)という記事があるのでこれを参考に`ActivityResultLauncher<Intent>`を使った方法を試します。`result.getResultCode()`で取り出した結果が`Activity.RESULT_OK`でなければ、メッセージをトーストで出して終了、そうでなければ次のユーザー操作なり、イベントを待つことになります。

```Java:BluetoothChatFragment.java
    @Override
    public void onStart() {
        super.onStart();
        if (mBluetoothAdapter == null) {
            return;
        }
        // If BT is not on, request that it be enabled.
        // setupChat() will then be called during onActivityResult
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
//            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
            mStartBluetoothAdapterEnable.launch(enableIntent);
        }
    }

    ActivityResultLauncher<Intent> mStartBluetoothAdapterEnable = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() != Activity.RESULT_OK) {
                    // User did not enable Bluetooth or an error occurred
                    FragmentActivity activity = getActivity();
                    if (activity != null) {
                        Toast.makeText(activity, R.string.bt_not_enabled_leaving,
                                Toast.LENGTH_SHORT).show();
                        activity.finish();
                    }
                }
            });
```
`R.string.bt_not_enabled_leaving` は `res/values/strings.xml`に `android-BluetoothChat`からコピー。これは、スマホの「設定」のBluetoothがOFFのまま、アプリを起動した際に、”「(アプリ名)」がBluetoothを ON にしようとしています”と表示され、許可されない時にトーストで出るメッセージです。

```xml:strings.xml
    <string name="bt_not_enabled_leaving">Bluetooth was not enabled. Leaving Bluetooth Chat.</string>

```

許可されるとスマホの「設定」にあるBluetoothがONになります。

## 接続先の指定

使用するAndroid端末とBluetooth端末(今回はPC)をペア設定する部分はAndroid端末の「設定」にある機能を利用し、アプリではペア設定済みの端末のリストから選択することにします。つまり、アプリ内で独自にBluetooth端末をスキャンしたり、ペア設定する機能は含まないことにします。従って、今回のアプリの接続実験をする際は、接続したいBluetooth端末とは事前にペア設定しておくことをお願いします。

アプリ内での接続先の選択は、`BluetoothChat`を踏襲し、画面のオプションメニューから選択できるようにします。
そのためには、`BluetoothChatFragment`に、`onCreateOptionsMenu`と`onOptionsItemSelected`を追加します。

```Java:BluetoothChatFragment.java
    @Override
    public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.bluetooth_menu, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.connect: {
                Log.d("onOptionsItemSelected", "push R.id.connect");
                // Launch the DeviceListActivity to see devices and do scan
                Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class);
//                startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE);
                mGetBluetoothAddress.launch(serverIntent);
                return true;
            }
        }
        return false;
    }

    ActivityResultLauncher<Intent> mGetBluetoothAddress = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        result -> {
            if (result.getResultCode() == Activity.RESULT_OK) {
                Bundle extras;
                if (result.getData() != null) {
                    extras = result.getData().getExtras();
                    address = extras.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
                    Log.d("mGetBluetoothAddress.getData ", "address = " + address);
                }
            }
        });
```
オプションメニューを開いて、押してほしいメニュー項目の名前を`strings.xml`に追加しておきます。

```xml:strings.xml
    <!-- Options Menu -->
    <string name="connect">Connect a device</string>
```

これで、オプションメニューから「Connect a device」と書かれたメニュー項目を選択すると、`DeviceListActivity`が呼ばれて接続先アドレスをユーザーに選択してもらい、その結果が戻ってくるので、`String address`で受け取るようにしておきます。

![OptionMenu](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/7f9a5abd-078b-05b6-4300-f1aee524ed48.png)
![Connect a device](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/7e1616a9-b9e2-bdd7-0b72-bb50eade6779.png)

ただし、ここも`startActivityForResult`は”非推奨”なので、`ActivityResultLauncher<Intent> mGetBluetoothAddress`
というメソッドを作りました。これを`startActivityForResult`の代わりに`launch`します。

また、`R.menu.bluetooth_menu` は、`res` ディレクトリに `menu` ディレクトリを作り、`bluetooth_menu.xml` を新規に作って、`BluetoothChat`の`res/menu/bluetooth_chat.xml`から必要な部分をコピーします。

``` Java:bluetooth_menu.xml
<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/connect"
        android:title="@string/connect"
        app:showAsAction="never" />

</menu>
```

secure かどうか、の部分は後で述べますが、使わないようにしましたので、選択肢は一つになりました。

## `setHasOptionsMenu()` deprecatedの対応

(2025年6月追記)[Fragment 1.5.0-alpha05](https://developer.android.com/jetpack/androidx/releases/fragment?hl=ja#1.5.0-alpha05) で `setHasOptionsMenu()` が deprecatedになったそうで、`onCreateOptionsMenu`、`onOptionsItemSelected`をoverrideしているところで「Overrides deprecated method in 'androidx.fragment.app.Fragment'」という警告が出るようになってしまいましたので、「[Menuの表示処理をMenuHostとMenuProviderに置き換える](https://qiita.com/Nabe1216/items/b26b03cbc750ac70a842)」を参考に変更しました。`onViewCreated`の処理の中に、`MenuHost`を追加し、それに`addMenuProvider`の中で`onCreateMenu()`、`onPrepareMenu()`をoverrideするようにすればよさそうです。合わせて、`onCreate`にあった`setHasOptionsMenu()`は不要になるので削除します。

```java        
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {

        MenuHost menuHost = requireActivity();
        menuHost.addMenuProvider(new MenuProvider() {
            @Override
            public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
                menuInflater.inflate(R.menu.option_menu, menu);
            }
            @Override
            public boolean onMenuItemSelected(@NonNull MenuItem item) {
                if (item.getItemId() == R.id.connect) {
                    Log.d("onOptionsItemSelected", "push R.id.connect");
                    // Launch the DeviceListActivity to see devices and do scan
                    Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class);
                    mGetBluetoothAddress.launch(serverIntent);
                    return true;
                }
                return false;
            }
        }, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
        ...
    }
```
他に、選択肢が１つならswitchでなくif文で、とか、`.length() == 0`ではなく、`address.isEmpty()`に置き換えた方がいいよ、とかのアドバイス(？warning)も出るので従って直しておきます。

## 接続の開始と終了

元々の`BlueToothChat`は、画面上でテキストを入力して、send ボタンを押すと送信されるようなユーザーインターフェースです。今回のアプリは「START」ボタンを押すと接続を開始し、定型の文字列を送信する、ということをやりたいので、自由にテキストを入力する部分は、なしにします。また、将来的にはそこそこ長時間、つなぎっ放しにしたいので、バックグラウンド(ForegroundService)で呼び出すように`startForegroundService(intent)`を使います。そうすると、停止するボタンを作っておかないと動きっぱなしになるので「STOP」ボタンで終了できるようにしておきます。その際は`stopService(intent)`を実行します。

また、うっかりOptionMenuから接続先を選ぶのを忘れて、いきなり「START」ボタンを押すと、`adress`がnullのままサービスが呼び出されてしまい接続時にエラーとなるので、nullのときや値が入っていないときは呼ばないようにし、トーストを出すようにしておきました。

```Java:BluetoothChatFragment.java
public class BluetoothChatFragment extends Fragment {
	...
	
    private Button mButtonStart;
    private Button mButtonStop;

	...
	

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        mButtonStart = view.findViewById(R.id.button_start);
        mButtonStart.setOnClickListener( v -> {
            if(address == null || address.length() == 0){
                FragmentActivity activity = getActivity();
                if (activity != null) {
                    Toast.makeText(activity, R.string.no_btAddress,
                            Toast.LENGTH_SHORT).show();
                    Log.d("button_start", getString(R.string.no_btAddress));
                }
            } else {
                Intent intent = new Intent(getActivity(), MyService.class);
                intent.putExtra("REQUEST_CODE", 1);
                intent.putExtra("BT_address", address);

                // Serviceの開始
                //startService(intent);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    requireActivity().startForegroundService(intent);
                }
            }
        });
        mButtonStop = view.findViewById(R.id.button_stop);
        mButtonStop.setOnClickListener( v -> {
            Log.d("debug", "button_stop");
            Intent intent = new Intent(getActivity(), BluetoothChatService.class);
            // Serviceの停止
            requireActivity().stopService(intent);
        });
    }
}
```

BluetoothChatFragment のレイアウトも指定が必要なので、そこにボタンの定義も併せて入れておきます。fragment_bluetooth_chat.xmlを/res/layout ディレクトリに新規で作ります。

``` xml:fragment_bluetooth_chat.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/button_start"
        android:text="@string/start"
        android:layout_margin="40dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/button_stop"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />

    <Button
        android:id="@+id/button_stop"
        android:text="@string/stop"
        android:layout_margin="40dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_start"
        />

</androidx.constraintlayout.widget.ConstraintLayout>
```
表示する文字も`strings.xml`も追加しておきます。

```xml:res/values/strings.xml
    <string name="start">Start</string>
    <string name="stop">Stop</string>
    <string name="no_btAddress">Bluetooth接続先が選択されていません</string>
```
画面イメージは以下です。

<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/ba31d910-1664-a072-a269-b655d86f79b2.png width="280px" alt="画面イメージ">

## BluetoothChatFragment のまとめ

元々の`BluetoothChat`の`BluetoothChatFragment.java`でBluetoothの使用が許可された場合、`onActivityResult()`にある`setupChat()`が実行されていますが、その中身はボタンを押したときの処理等の定義で、前述のように今回はバックグラウンド(ForegroundService)で使いたいので、この部分は使いませんでした。その他、statusの操作や、handlerはとりあえず不要なので使いません。

生き残った部分は、以下のようになりました。

```Java:BluetoothChatFragment.java
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;

public class BluetoothChatFragment  extends Fragment {
    /**
     * Local Bluetooth adapter
     */
    private BluetoothAdapter mBluetoothAdapter = null;

    String address;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Get local Bluetooth adapter
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        // If the adapter is null, then Bluetooth is not supported
        FragmentActivity activity = getActivity();
        if (mBluetoothAdapter == null && activity != null) {
            Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show();
            activity.finish();
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (mBluetoothAdapter == null) {
            return;
        }
        // If BT is not on, request that it be enabled.
        // setupChat() will then be called during onActivityResult
        if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
//            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
            mStartBluetoothAdapterEnable.launch(enableIntent);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false);
    }

    ActivityResultLauncher<Intent> mStartBluetoothAdapterEnable = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() != Activity.RESULT_OK) {
                    // User did not enable Bluetooth or an error occurred
                    FragmentActivity activity = getActivity();
                    if (activity != null) {
                        Toast.makeText(activity, R.string.bt_not_enabled_leaving,
                                Toast.LENGTH_SHORT).show();
                        activity.finish();
                    }
                }
            });

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        MenuHost menuHost = requireActivity();
        menuHost.addMenuProvider(new MenuProvider() {
            @Override
            public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
                menuInflater.inflate(R.menu.bluetooth_menu, menu);
            }
            @Override
            public boolean onMenuItemSelected(@NonNull MenuItem item) {
                if (item.getItemId() == R.id.connect) {
                    Log.d("onOptionsItemSelected", "push R.id.connect");
                    // Launch the DeviceListActivity to see devices and do scan
                    Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class);
//                startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE);
                    mGetBluetoothAddress.launch(serverIntent);
                    return true;
                }
                return false;
            }
        }, getViewLifecycleOwner(), Lifecycle.State.RESUMED);

        Button mButtonStart = view.findViewById(R.id.button_start);
        mButtonStart.setOnClickListener(v -> {
            Log.d("debug", "button_start");
            if(address == null || address.isEmpty()){
                FragmentActivity activity = getActivity();
                if (activity != null) {
                    Toast.makeText(activity, R.string.no_btAddress,
                            Toast.LENGTH_SHORT).show();
                    Log.d("button_start", getString(R.string.no_btAddress));
                }
            } else {
                Intent intent = new Intent(getActivity(), MyService.class);
                intent.putExtra("REQUEST_CODE", 1);
                intent.putExtra("BT_address", address);

                // Serviceの開始
                //startService(intent);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    requireActivity().startForegroundService(intent);
                }
            }
        });
        Button mButtonStop = view.findViewById(R.id.button_stop);
        mButtonStop.setOnClickListener(v -> {
            Log.d("debug", "button_stop");
            Intent intent = new Intent(getActivity(), MyService.class);
            // Serviceの停止
            requireActivity().stopService(intent);
        });
    }

    ActivityResultLauncher<Intent> mGetBluetoothAddress = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Bundle extras;
                    if (result.getData() != null && result.getData().getExtras() != null) {
                        extras = result.getData().getExtras();
                        address = extras.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
                        Log.d("mGetBluetoothAddress.getData ", "address = " + address);
                    }
                }
            });

}
```

# DeviceListActivity.java

ペア設定済みの端末のリストから接続先を選択する部分は`DeviceListActivity.java`です。
まだ`BluetoothChat`から[`DeviceListActivity.java`](https://github.com/googlearchive/android-BluetoothChat/blob/d6e5db25764fe94f294eb1b6e9ea521dc2f02c71/Application/src/main/java/com/example/android/bluetoothchat/DeviceListActivity.java)をコピーしていなかったので、これもMainActivityと同列の場所にコピー。

```Java:DeviceListActivity.java

import android.Manifest;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

import androidx.core.app.ActivityCompat;

import java.util.Set;

/**
 * This Activity appears as a dialog. It lists any paired devices.
 * When a device is chosen by the user,
 * the MAC address of the device is sent back to the parent
 * Activity in the result Intent.
 */
public class DeviceListActivity extends Activity {

    /**
     * Tag for Log
     */
    private static final String TAG = "DeviceListActivity";

    /**
     * Return Intent extra
     */
    public static String EXTRA_DEVICE_ADDRESS = "device_address";

    /**
     * Member fields
     */
    private BluetoothAdapter mBtAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Log.d(TAG, "onCreate");
        // Setup the window
//        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        setContentView(R.layout.activity_device_list);

        // Set result CANCELED in case the user backs out
        setResult(Activity.RESULT_CANCELED);

        // Initialize array adapters. One for already paired devices and
        // one for newly discovered devices
        ArrayAdapter<String> pairedDevicesArrayAdapter =
                new ArrayAdapter<>(this, R.layout.device_name);

        // Find and set up the ListView for paired devices
        ListView pairedListView = findViewById(R.id.paired_devices);
        pairedListView.setAdapter(pairedDevicesArrayAdapter);
        pairedListView.setOnItemClickListener(mDeviceClickListener);

        // Get the local Bluetooth adapter
        mBtAdapter = BluetoothAdapter.getDefaultAdapter();

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
                && ActivityCompat.checkSelfPermission(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.
            Log.d(TAG, "checkSelfPermission != PackageManager.PERMISSION_GRANTED");
            return;
        }
        // Get a set of currently paired devices
        Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
        Log.d(TAG, "pairedDevices.size() = " + pairedDevices.size());

        // If there are paired devices, add each one to the ArrayAdapter
        if (pairedDevices.size() > 0) {
            findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
            for (BluetoothDevice device : pairedDevices) {
                pairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
                Log.d(TAG, "deviceName = " + device.getName() + ",Address = " + device.getAddress());
            }
        } else {
            String noDevices = getResources().getText(R.string.none_paired).toString();
            pairedDevicesArrayAdapter.add(noDevices);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    /**
     * The on-click listener for all devices in the ListViews
     */
    private AdapterView.OnItemClickListener mDeviceClickListener
            = new AdapterView.OnItemClickListener() {
        public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {

            // Get the device MAC address, which is the last 17 chars in the View
            String info = ((TextView) v).getText().toString();
            String address = info.substring(info.length() - 17);

            // Create the result Intent and include the MAC address
            Intent intent = new Intent();
            intent.putExtra(EXTRA_DEVICE_ADDRESS, address);

            // Set result and finish this Activity
            setResult(Activity.RESULT_OK, intent);
            finish();
        }
    };
}
```
ただし、新規のデバイスを探したり、ペア設定する部分はアプリ内でやらないつもりなので、その部分は削除してあります。また、オリジナルでは、`requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS)` となっているところが取り消し線表示になったのでカーソルを持っていくと

	'FEATURE_INDETERMINATE_PROGRESS' is deprecated as of API 24: Android 7.0 (Nougat)

が出ました。[ActionbarにprogressBarを表示する](https://qiita.com/kentosasa/items/7199016b290d11411ba4)そうですが、とりあえずコメントアウトで対応しました。さらに、`mBtAdapter.getBondedDevices()`も

	Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`

と言われるので修正候補を選択すると、この行の前に
```Java
        if (ActivityCompat.checkSelfPermission(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;
        }
```
が挿入されました。とりあえず、ここはこれに従っています。

なお、`BLUETOOTH_CONNECT`の権限は今後徐々に減ってくるとは思ますが、Android 11以下では不要なので、もし実験したいスマホが古いものだった場合は接続したいデバイスがペア設定してあっても接続先リストが出てこない可能性があります。実際、筆者もちょっとあせりました。その場合は

```Java
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
            && ActivityCompat.checkSelfPermission(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;
        }
```
としておくとペア設定済みデバイスリストが表示されると思います。

最後に`pairedListView`の`setOnItemClickListener`で渡されている`mDeviceClickListener`の設定があります。`DeviceListActivity`で画面に表示されたペア設定済みデバイスのリストの中から、今回接続したいデバイスを選択してもらい、`Intent`に入れて呼び出しに送り返しています。


`res/layout/activity_device_list.xml`も”ペア設定済み”の部分だけ`BluetoothChat`からコピー。

```xml:activity_device_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/title_paired_devices"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#666"
        android:paddingLeft="5dp"
        android:text="@string/title_paired_devices"
        android:textColor="#fff"
        android:visibility="gone"
        />

    <ListView
        android:id="@+id/paired_devices"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:stackFromBottom="true"
        />
</LinearLayout>
```

`res/layout/device_name.xml`も`BluetoothChat`からコピー。

```xml:device_name.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:padding="5dp"
          android:textSize="18sp"
    />
```

`strings.xml`に以下を追加

```xml:strings.xml
    <!--  DeviceListActivity -->
    <string name="select_device">select a device to connect</string>
    <string name="none_paired">No devices have been paired</string>
    <string name="title_paired_devices">Paired Devices</string>
```

後でもう一度出てきますが、新しいアクティビティとして、マニフェストに忘れず追加しておきます。

```xml:AndroidManifest.xml
        <activity
            android:name=".DeviceListActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/select_device"
            android:theme="@android:style/Theme.Holo.Dialog"/>
```

接続先選択画面はこんな感じです。もちろん、ペア設定済みデバイスの数は同じではありませんので、ペア設定済みデバイスの数に応じてリストアップされるデバイスの数は変化します。

![接続先選択画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/a20ea2c0-f813-1a0b-bd56-c93ee7e165f1.png)


# BluetoothConnection.java
## 概要

元々の`BluetoothChat`の[`BluetoothChatService.java`](https://github.com/googlearchive/android-BluetoothChat/blob/d6e5db25764fe94f294eb1b6e9ea521dc2f02c71/Application/src/main/java/com/example/android/bluetoothchat/BluetoothChatService.java)の中身は、`start`、`connect`等のメソッドと、`AcceptThread`、`ConnectThread`、`ConnectedThread`があります。`AcceptThread`は、[「Bluetoothの概要」](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)の[「サーバー側の接続」](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja#ConnectAsAServer)にも類似のものが出てきますが、今回はPC側からスマホを接続する構成は考えていませんので使わないことにします。
また、バックグラウンド(ForegroundService)を使う関係で、バックグラウンドで動かすのに必要な処理をまとめた`MyService`と、bluetooth接続処理を新しく`BluetoothConnection.java`というクラスを作って分割し、`MyService`から`BluetoothConnection`のメソッドを呼ぶ構成にしました。

## ConnectThread
クライアント側として接続する部分を確認します。「Bluetoothの概要」の[「クライアント側の接続」](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja#ConnectAsAClient)と見比べると、`BluetoothChat`の`BluetoothChatService.java`の`ConnectThread`の方は、引数に`boolean secure`が追加されていて、`BluetoothSocket mmSocket`を`BluetoothDevice mmDevice`と関連付け、`secure` が true のときは`MY_UUID_SECURE`を選択、`Insecure`のときは`MY_UUID_INSECURE`を選択肢するところが追加になっています。

secure かどうかでUUIDを分ける？と探したところ、[「Bluetooth通信の実装|Android開発」](https://moewe-net.com/android/connect-bluetooth)にあるように、 SPP (シリアルポートプロファイル) の UUID ("00001101-0000-1000-8000-00805f9b34fb")を使えばよさそうなので、使ってみます。ところが、実際にこのコードをコピーしてみると、`mDevice.createRfcommSocketToServiceRecord(SERIAL_PORT_PROFILE)`部分に赤い波線が出て、ここでも

    	Call requires permission which may be rejected by user: code should explicitly check to see if permission is available (with `checkPermission`) or explicitly handle a potential `SecurityException`

が出ますので、パーミッションチェックを入れます。
```java
            try {
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
                        && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                mSocket = mDevice.createRfcommSocketToServiceRecord(SERIAL_PORT_PROFILE);
            } catch (IOException e) {
                Log.w(TAG, "BluetoothConnection couldn't be established due to an exception: " + e);
                mSocket = null;
            }

```

`mSocket.connect()`も同様です。そうなると、Contextを持って来なければいけなくなるので、コンストラクタにcontextを引数に追加しておき、呼び出し側のサービスのContextを渡すようにしました。

```java
    public BluetoothConnection(Context _context, String bluetoothAddress) {
        this(_context, BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bluetoothAddress));
    }

    public BluetoothConnection(Context _context, BluetoothDevice device) {
        context = _context;

        mDevice = Objects.requireNonNull(device);
    }
```

`ConnectThread`はメインスレッドではありませんので、接続処理をしても成功したのか外からは分かりませんので、接続成功/失敗を知らせてくれるリスナーも用意します。

```java
    /** Bluetooth接続が確立・切断された通知を受けるクラスが実装するインタフェース */
    public interface connectionInterface{
        /** Bluetooth接続の確立通知を受けるメソッド */
        void onBluetoothConnected();
        /** Bluetooth接続失敗の通知を受けるメソッド */
        void onBluetoothConnectFailed();
    }

```
`mSocket.connect()`でエラーが出たら(catch文の方に処理が移行したら) `onBluetoothConnectFailed()` を、無事終了したら`onBluetoothConnected()`が呼ばれるようにしておきます。

## BluetoothConnection.java のまとめ
以上の内容をひとつにまとめると、以下のようになりました。

```Java:BluetoothConnection.java
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

import androidx.core.app.ActivityCompat;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import java.util.UUID;

public class BluetoothConnection {
    // Debugging
    private static final String TAG = "BluetoothConnection";

    Context context;

    /**
     * This is the well-known UUID for the Bluetooth SPP (Serial Port Profile)
     */
    private static final UUID SERIAL_PORT_PROFILE = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

    private final BluetoothDevice mDevice;
    private BluetoothSocket mSocket = null;

    // Member fields
    private ConnectThread mConnectThread;

    /** Bluetooth接続が確立・切断された通知を受けるクラスが実装するインタフェース */
    public interface connectionInterface{
        /** Bluetooth接続の確立通知を受けるメソッド */
        void onBluetoothConnected();
        /** Bluetooth接続失敗の通知を受けるメソッド */
        void onBluetoothConnectFailed();
    }

    private connectionInterface connectionListener = null;
    /** リスナを設定するメソッド */
    public void setConnectionListener(connectionInterface listener) {
        connectionListener = listener;
    }

    public BluetoothConnection(Context _context, String bluetoothAddress) {
        this(_context, BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bluetoothAddress));
    }

    public BluetoothConnection(Context _context, BluetoothDevice device) {
        context = _context;

        mDevice = Objects.requireNonNull(device);
    }

//    @Override
    public String getAddress() {
        return mDevice.getAddress();
    }

    /**
     * Establish an RFCOMM connection to the remote device.
     *
     * Assumes there is no existing connection.
     *
     * This method may take time to return (or even not return in pathological cases).
     * It is a good idea to wrap it in some kind of Promise-like object.
     *
     * @return true if it could connect, false otherwise
     */
    public void connect() {
        // Start the thread to connect with the given device
        mConnectThread = new ConnectThread();
        mConnectThread.start();
    }

//    @Override
    public boolean isConnected() {
        return mSocket != null && mSocket.isConnected();
    }

    public synchronized void close() {

        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        if (isConnected()) {
            try {
                mSocket.close();
                Log.d(TAG, "mSocket.close");
            } catch (IOException e) {
                // we are letting go of the connection anyway, so log and continue
                Log.w(TAG, "IOException during BluetoothSocket close(): " + e);
            } finally {
                mSocket = null;
            }
        } else {
            Log.d(TAG, "is NOT Connected");
        }
    }

//    @Override
    public InputStream getInputStream() {
        if (isConnected()) {
            try {
                return mSocket.getInputStream();
            } catch (IOException e) {
                Log.w(TAG, "failed to get Bluetooth input stream: " + e);
            }
        }
        return null;
    }

//    @Override
    public OutputStream getOutputStream() {
        if (isConnected()) {
            try {
                return mSocket.getOutputStream();
            } catch (IOException e) {
                Log.w(TAG, "failed to get Bluetooth output stream: " + e);
            }
        }
        return null;
    }

    /**
     * This thread runs while attempting to make an outgoing connection
     * with a device. It runs straight through; the connection either
     * succeeds or fails.
     */
    private class ConnectThread extends Thread {

        public ConnectThread() {

            // Get a BluetoothSocket for a connection with the
            // given BluetoothDevice
            try {
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
                        && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                mSocket = mDevice.createRfcommSocketToServiceRecord(SERIAL_PORT_PROFILE);
            } catch (IOException e) {
                Log.w(TAG, "BluetoothConnection couldn't be established due to an exception: " + e);
                mSocket = null;
            }
        }

        public void run() {
            Log.d("BEGIN ConnectThread", "Thread name = " + Thread.currentThread().getName());


            // Make a connection to the BluetoothSocket

            try {
                // This is a blocking call and will only return on a
                // successful connection or an exception
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S
                        && ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                mSocket.connect();
            } catch (IOException e) {
                // Close the socket
                try {
                    mSocket.close();
                } catch (IOException e2) {
                    Log.e(TAG, "unable to close() " +
                            " socket during connection failure", e2);
                }
                Log.d(TAG, "connectionFailed", e);
                if(connectionListener != null){
                    connectionListener.onBluetoothConnectFailed();
                }
                return;
            }

            // Reset the ConnectThread because we're done
            synchronized (BluetoothConnection.this) {
                Log.d(TAG, "mConnectThread = null");
                mConnectThread = null;
            }

            Log.d(TAG, "Connected !");
            if(connectionListener != null){
                connectionListener.onBluetoothConnected();
            }
        }

        public void cancel() {
            try {
                Log.d(TAG, "mSocket.close()");
                mSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "socket failed", e);
            }
        }
    }
}
```

# MyService.java

`MyService.java`では、バックグラウンド(ForegroundService)開始のための処理と、サービスを開始したら`BluetoothConnection`のインスタンスを作成し、接続する`connect`メソッドを呼び出すメソッド`connectDevice(String address)`、接続処理が完了したら文字列を送信するスレッド`ConnectedThread`を用意しました。

サービスを作成するには、onStartCommand()が呼び出されますが、の`PendingIntent.getActivity(Context, int, Intent, int)`の最後の`int`は、`int flags`で、今回は`PendingIntent.FLAG_IMMUTABLE`にしています。以前のサンプルだと`PendingIntent.FLAG_UPDATE_CURRENT`になっているものがありますが、実行時にエラーが出て、Android Studioに

	Strongly consider using FLAG_IMMUTABLE

と怒られたのでそうしてあります。[「ほとんどケースで FLAG_IMMUTABLE を指定することになると思います」](https://star-zero.medium.com/pendingintent%E3%81%AEflag-immutable%E3%81%A8flag-mutable-d72bf7c90135)だそうです。

さらに、アプリ画面上の「STOP」ボタンを押すと、`MyService` の `onDestroy()` が呼ばれるので、ループ処理フラグをOFF(`onActive = false`)、for ループ `break`となる処理も追加しておきます。


``` Java:MyService.java
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;

public class MyService extends Service implements BluetoothConnection.connectionInterface {

    private BluetoothConnection mBluetoothConnection;

    ConnectedThread thread;

    public IBinder onBind(Intent intent) {
        return null;
    }

    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("MyService", "onStartCommand()");
        Log.d("MyService", "Thread name = " + Thread.currentThread().getName());

        int requestCode = intent.getIntExtra("REQUEST_CODE",0);
        Context context = getApplicationContext();
        String channelId = "default";
        String title = context.getString(R.string.app_name);

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {

            PendingIntent pendingIntent =
                    PendingIntent.getActivity(context, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);

            NotificationManager notificationManager =
                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

            // Notification Channel 設定
            NotificationChannel channel = new NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT);

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);

                Notification notification = new Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        // android標準アイコンから
                        .setSmallIcon(android.R.drawable.ic_media_play)
                        .setContentText("MediaPlay")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);

            }
        }

        String address = intent.getStringExtra("BT_address");

        if(mBluetoothConnection == null){
            connectDevice(address);
        } else {
            mBluetoothConnection.close();
            mBluetoothConnection = null;
            connectDevice(address);
        }

        return START_NOT_STICKY;
    }
    @Override
    public void onDestroy(){
        thread.cancel();
    }

    private void connectDevice(String address){
        // Get the BluetoothDevice object
        Log.d("connectDevice ", "address = " + address);
        mBluetoothConnection = new BluetoothConnection(this, address);
        mBluetoothConnection.setConnectionListener(this);
        mBluetoothConnection.connect();
    }

    @Override
    public void onBluetoothConnected() {
        thread = new ConnectedThread();
        thread.start();
    }

    @Override
    public void onBluetoothConnectFailed() {
        Log.d(getClass().getSimpleName(), "onBluetoothConnectFailed");
    }

    private class ConnectedThread extends Thread {
        boolean onActive = true;
        OutputStream out = Objects.requireNonNull(mBluetoothConnection.getOutputStream());

        public void run() {
            Log.d("ConnectedThread run", "Thread name = " + Thread.currentThread().getName());
            try {
                out.write(("Hello!" + "\r").getBytes());
                out.flush();
                synchronized (this) {
                    for (int i = 0; i < 10; i++) {
                        out.write((" i = " + i+ "\r").getBytes());
                        try {
                            wait(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if(!onActive) break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d("ConnectedThread", "end");
        }
        public void cancel(){
            onActive = false;
            Log.d("ConnectedThread", "cancel");
        }
    }
}
```

# AndroidManifest

## manifestへのパーミッションの追加
まず、Bluetoothを使う場合は[Bluetooth の概要](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)にある通り、`BLUETOOTH`と`ACCESS_FINE_LOCATION`のパーミッションが必要だそうで、manifestに追加します。
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication">

  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

  <!-- If your app targets Android 9 or lower, you can declare
       ACCESS_COARSE_LOCATION instead. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  ...

```

すると、Android Studioに

```
If you need access to FINE location, you must request both `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION`
```
と怒られるのでそれを追加。Android 12（API レベル 31）からBluetooth接続するために`BLUETOOTH_CONNECT`も必要になるのでこれも追加します。さらに、bluetooth接続をバックグラウンド(ForegroundService)で実施したいので、Android 9（API レベル 28）以降では`FOREGROUND_SERVICE`を追記する必要があるでこれも追加します。

(2025年6月追記)Android 14 (APIレベル34)からは、マニフェストでフォアグラウンド サービスのタイプを申告する、選択したタイプに応じて、フォアグラウンド サービスのタイプごとに適したフォアグラウンド サービスの権限を申告し、リクエストする([フォアグラウンド サービスと全画面インテントの要件について](https://support.google.com/googleplay/android-developer/answer/13392821?hl=ja))そうなので、[フォアグラウンド サービス タイプは必須](https://developer.android.com/about/versions/14/changes/fgs-types-required?hl=ja#permission-for-fgs-type)を参考に、「接続されたデバイス」を追加します。

```xml
    <!-- API 28 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <!-- API 34 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
```

## Activity、service
manifestには、Empty Activityに含まれるMainActivityが既に記述されていますので、これまでで出てきた DeviceListActivity を追加しておきます。さらに、serviceも記載しておかないと起動できないので、忘れず追加します。

(2025年6月追記)ここも、serviceに'foregroundServiceType'の指定が必要だそうなので追加しています。

```xml:AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication">

    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!-- If your app targets Android 9 or lower, you can declare
         ACCESS_COARSE_LOCATION instead. -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- API 28 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <!-- API 34 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />

    <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.MyApplication"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".DeviceListActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/select_device"
            android:theme="@android:style/Theme.Holo.Dialog"/>
        <service
            android:name="com.example.myapplication.MyService"
            android:foregroundServiceType="connectedDevice" />
    </application>
</manifest>
```

# Bluetooth接続の実機テスト

Bluetoothの接続確認を、受信内容が確認できるようにAndroid端末とPCを使って受信確認をしたいと思います。PCには今回、ターミナルエミュレータとして「TeraTerm」をインストールし、[「Windows を Bluetooth SPP Server として使用する方法」](https://kokufu.blogspot.com/2014/02/windows-bluetooth-spp-server.html)等を参考にTeraTermを設定しました。

1. PCの設定からBluetoothをオンに、PCの名前を確認
1. Android端末とペア設定
1. TeraTerm の「新しい接続」画面で「Bluetoothリンク経由の標準シリアル（COM*）」で「OK」を押下
1. Android端末の今回のアプリのオプションメニューから、上のPCの名前を探して選択
1. アプリの「START」ボタンを押す

すると、`BluetoothConnection`の`ConnectThread`の`run()`に設定どおり、"Hello!"とPC側のTeraTermの画面に表示され、i = 0からカウントアップした表示が続き、PCに文字列が送信できたことが分かりました。
![Teratermで受信](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/6206c3c4-9844-5957-07d8-8120501f02b9.png)
また、途中で「STOP」ボタンを押すと、カウントアップは中止されますし、接続先を選択しないで「START」ボタンを押すと、設定どおり「Bluetooth接続先が選択されていません」の表示が出ることも確認できました。

# Gradleバージョン
(2025年6月追記)この記事を書いたときのコードを久しぶりに新しいAndroid Studioで開いてみると、ビルドエラー Gradle project sync failedが出たのでアップグレードしました。今のところ、Gradle version が 8.14.2、gradle plugin version 8.10.1 がお薦めだそうなので、それにしました。合わせて、いろんなものをバージョンアップすることを薦めてきたので本文のサンプルコードも更新しました。

# 参考
+ [https://github.com/googlesamples/android-BluetoothChat](https://github.com/googlesamples/android-BluetoothChat)
+ [Android開発日記　AndroidのBluetooth通信（SPP）【初心者向け】](https://mikulogi-tomo.hatenablog.com/entry/2018/01/04/033458)
+ [アプリの権限をリクエストする](https://developer.android.com/training/permissions/requesting?hl=ja)
+ [アクティビティの結果を取得する](https://developer.android.com/training/basics/intents/result)
+ [Bluetooth の概要](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)
+ [Bluetooth通信の実装|Android開発](https://moewe-net.com/android/connect-bluetooth)
+ [PendingIntentのFLAG_IMMUTABLEとFLAG_MUTABLE](https://star-zero.medium.com/pendingintent%E3%81%AEflag-immutable%E3%81%A8flag-mutable-d72bf7c90135)
+ [Windows を Bluetooth SPP Server として使用する方法](https://kokufu.blogspot.com/2014/02/windows-bluetooth-spp-server.html)
+ [フォアグラウンド サービスと全画面インテントの要件について](https://support.google.com/googleplay/android-developer/answer/13392821?hl=ja)
+ [Menuの表示処理をMenuHostとMenuProviderに置き換える](https://qiita.com/Nabe1216/items/b26b03cbc750ac70a842)