Android端末とOBD2スキャンツールを使って車の内部データを取得するアプリに車のキー操作(電源接続)と連動してデータを取得する機能を追加

# はじめに

「[AndroidスマホとOBD2アダプターを使って車の内部情報を取得するのに「obd2-lib」を活用](https://qiita.com/naoto_deuxchats/items/33564ad8eec4ad310041)」で、OBD2スキャンツール経由で車の内部データを取得しました。ただ、走行するたびにアプリのスタートボタン、ストップボタンを押すのも面倒だし、忘れてしまうこともあるので、車のシガーソケット等から充電用のUSBケーブルを接続しておき、キー操作(電源供給)と連動してデータ取得をスタート、ストップできるようにしました。

https://qiita.com/naoto_deuxchats/items/33564ad8eec4ad310041

# 検証環境

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

# デバイスが充電されているかどうかを検出する

Android Developersの「[電池残量と充電状態を監視する](https://developer.android.com/training/monitoring-device-state/battery-monitoring?hl=ja)」には

> BroadcastReceiver をマニフェストで登録し、ACTION_POWER_CONNECTED と ACTION_POWER_DISCONNECTED をインテント フィルタ内で定義することで両方のイベントをリッスンする必要があります。

とあります。一方で、「[Android 充電ケーブル挿す、抜くのイベントを拾う](http://pdfandfont.blogspot.com/2020/06/android.html)」には、マニフェストに登録して検出できるのはAndroid 7までで、Android 8以降は `BroadcastReceiver`をレシーバーとして登録する、かつ、アプリが起きていないと検出できない、と書いてありました。この記事には、「[ブロードキャストの概要](https://developer.android.com/guide/components/broadcasts?hl=ja)」も参考にした、と書いてありましたので、確認すると、Android 8.0（API レベル 26）以降では制限が増えているようですね。ということで、`IntentFilter`と`BroadcastReceiver`を`MainActivity.java`に追加し、`onCreate()`でブロードキャストレシーバーを登録して電源接続、切断が受信できるようにし、`onDestroy()`で解除するようにして、電源接続、切断が検出できるか確認しました。

```java:MainActivity.java

package com.example.myapplication;

import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity{

    PowerConnectionReceiver mReceiverPowerConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("debug", "onCreate");

        mReceiverPowerConnection = new PowerConnectionReceiver();

        // インテントフィルタ
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
        filter.addAction(Intent.ACTION_POWER_CONNECTED);
        filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
        // ブロードキャストレシーバ登録
        registerReceiver(mReceiverPowerConnection, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("debug", "onDestroy");
        unregisterReceiver(mReceiverPowerConnection);
    }
}
```
`BroadcastReceiver`には電源接続、切断を検出した時の処理を追加したいので、`BroadcastReceiver`を継承した新しいクラスを作りました。ブロードキャストレシーバーで受信する`Intent`の[`getAction()`](https://developer.android.com/reference/android/content/Intent#getAction())が`Intent.ACTION_BATTERY_CHANGED`だった時にバッテリーの情報が取得できるようで、そこからさらに[`getIntExtra(String name, int defaultValue)`](https://developer.android.com/reference/android/content/Intent#getIntExtra(java.lang.String,%20int))で`BatteryManager.EXTRA_PLUGGED`をキーとして値を取り出すことで電源が接続されているかどうか、接続先がAC電源か、USBか、が分かるようになっているそうです。実際にPCとの接続で実験すると、デバイスの接続/切断で色々メッセージとかも出て分かりにくいので、`Toast`も追加しておきます。試してみると、PCのUSBポートから接続しても、`BATTERY_PLUGGED_AC`のトーストが出るので、たいていの場合はAC電源と認識するようです。

```java:PowerConnectionReceiver.java

package com.example.myapplication;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.BatteryManager;
import android.util.Log;
import android.widget.Toast;

import java.util.Objects;

public class PowerConnectionReceiver extends BroadcastReceiver {
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getName() ;

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(Objects.requireNonNull(action).equals(Intent.ACTION_BATTERY_CHANGED)) {
            int nowPlugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
            if (nowPlugged == BatteryManager.BATTERY_PLUGGED_AC) {        // int android.os.BatteryManager.BATTERY_PLUGGED_AC : 1 [0x1]
                Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_AC");
                Toast.makeText(context, "BATTERY_PLUGGED_AC", Toast.LENGTH_LONG ).show();
            } else if (nowPlugged == BatteryManager.BATTERY_PLUGGED_USB) {
                //	int android.os.BatteryManager.BATTERY_PLUGGED_USB : 2 [0x2]
                Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_USB");
                Toast.makeText(context, "BATTERY_PLUGGED_USB", Toast.LENGTH_LONG ).show();
            } else if (nowPlugged == 0) {
                Log.d(TAG, "Unplugged.");
                Toast.makeText(context, "Unplugged.", Toast.LENGTH_LONG ).show();
            } else {
                Log.d(TAG,"nowPlugged = " + nowPlugged);
            }
        }
    }
}
```

実機デバッグしてみると、`Intent.ACTION_BATTERY_CHANGED`が例えば充電残量が減ったり、何か状態が変わるたびに送られてくるようで、PCに接続しっぱなしだとDebug画面にLogが何度も表示されます。

![ケーブル接続状態.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/fba92c5f-da2e-7e32-3095-47412e0c046d.png)


やりたかったのは、接続した時（電圧がかかった時）、ケーブルを抜いた時(電圧が下がった時）を検出したいということだったので、状態を記憶する変数を一個追加し、状態が変化した時、という条件を追加します。ここでは、PowerConnectionReceiver.javaにstatic 変数を追加し、前回値と違う場合のみ、Log、Toastを出すようにしました。

![接続時と切断時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/8e33d57b-9904-70e6-e4f6-a07553e059a4.png)


```java:PowerConnectionReceiver.java

public class PowerConnectionReceiver extends BroadcastReceiver {
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getName() ;

    private static int mPluggedLast = -1;

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(Objects.requireNonNull(action).equals(Intent.ACTION_BATTERY_CHANGED)) {
            int nowPlugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
            if (mPluggedLast != nowPlugged) {
                if (nowPlugged == BatteryManager.BATTERY_PLUGGED_AC) {        // int android.os.BatteryManager.BATTERY_PLUGGED_AC : 1 [0x1]
                    Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_AC");
                    Toast.makeText(context, "BATTERY_PLUGGED_AC", Toast.LENGTH_LONG ).show();
                } else if (nowPlugged == BatteryManager.BATTERY_PLUGGED_USB) {
                    //	int android.os.BatteryManager.BATTERY_PLUGGED_USB : 2 [0x2]
                    Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_USB");
                    Toast.makeText(context, "BATTERY_PLUGGED_USB", Toast.LENGTH_LONG ).show();
                } else if (nowPlugged == 0) {
                    Log.d(TAG, "Unplugged.");
                    Toast.makeText(context, "Unplugged.", Toast.LENGTH_LONG ).show();
                } else {
                    Log.d(TAG,"nowPlugged = " + nowPlugged);
                }
                mPluggedLast = nowPlugged;
            }
        }
    }
}
```

# 接続状態の確認ができる可能性を増やす
この状態でも、画面が消灯していたり、ほかのアプリを使っていても検出してくれるようですが、アプリを間違ってクリア(終了)してしまったり、ある程度の時間がたった後に"Doze"モードに入ってシステムで制限をかけて電池の消費を抑える、なんてこともあるそうなので、接続状態の確認ができる可能性を増やす方法として、フォアグラウンドサービス(Foreground services)を試してみます。

## フォアグラウンドサービスの設定
「[【Android】バックグラウンド(フォアグラウンド サービス)とマルチスレッド(Thread)の動きを簡単な事例で実験的に調査](https://qiita.com/naoto_deuxchats/items/447d609166bc639c9436)」の「[Foreground Service](https://qiita.com/naoto_deuxchats/items/447d609166bc639c9436#foreground-service)」を基に、`ReceivingService`という名前で`Service`を継承したクラスを作りました。中身のほとんどはお約束の`NotificationManager`や`NotificationChannel`の設定です。通知としては、アプリの名前(今回はMy application)と、「待機中...」と表示させてみました。
次に、`onStartCommand()`メソッドにインテントフィルター追加とブロードキャストレシーバーを登録しています。こうすることで、アプリがクリアされてもバッテリーの情報がキャッチできるはずです。

```java:ReceivingService.java

package com.example.myapplication;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.BatteryManager;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;

import java.util.Objects;

public class ReceivingService extends Service {

    PowerConnectionReceiver mReceiverPowerConnection;

    private static int mPluggedLast = -1;

    @Override
    public void onCreate() {
        mReceiverPowerConnection = new PowerConnectionReceiver();
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getName() ;
        Log.d(TAG, "Start");

        Context context = getApplicationContext();

        int requestCode = 0;
        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);

            // ForegroundにするためNotificationが必要、Contextを設定
            NotificationManager notificationManager =
                    (NotificationManager) context.
                            getSystemService(Context.NOTIFICATION_SERVICE);

            // Notification Channel 設定
            NotificationChannel channel = new NotificationChannel(
                    channelId, title, NotificationManager.IMPORTANCE_DEFAULT);
            channel.setDescription("Silent Notification");
            // 通知音を消さないと毎回通知音が出てしまう
            // この辺りの設定はcleanにしてから変更
            channel.setSound(null, null);
            // 通知ランプを消す
            channel.enableLights(false);
            channel.setLightColor(Color.BLUE);
            // 通知バイブレーション無し
            channel.enableVibration(false);

            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
                Notification notification = new Notification.Builder(context, channelId)
                        .setContentTitle(title)
                        .setSmallIcon(android.R.mipmap.sym_def_app_icon)
                        //		.setSmallIcon(android.R.drawable.sym_def_app_icon)
                        .setContentText(getString(R.string.waiting))
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);
            }
        } else {
            Notification notification = new Notification.Builder(this)
                    .setContentTitle(title)
                    .setContentText(getString(R.string.waiting))
//				.setContentIntent(pendingIntent)	//	通知からの起動なし
                    .setSmallIcon(android.R.mipmap.sym_def_app_icon)
                    //		.setSmallIcon(android.R.drawable.sym_def_app_icon)
                    .setAutoCancel(true)
                    .build();
            startForeground(1, notification);

        }

        // インテントフィルタ
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
        filter.addAction(Intent.ACTION_POWER_CONNECTED);
        filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
        // ブロードキャストレシーバ登録
        registerReceiver(mReceiverPowerConnection, filter);

        filter = new IntentFilter();
        filter.addAction("DO_ACTION");
        registerReceiver(mReceiverPowerConnection, filter);
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

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

        unregisterReceiver(mReceiverPowerConnection);
    }
}
```

```xml:res/values/strings.xml
<resources>
	...	
    <!-- ReceivingService -->
    <string name="waiting">待機中...</string>
</resources>
```

## MainActivityの変更

`onCreate`で`startForegroundService()`を実行することでフォアグラウンドサービスを開始しています。アンドロイドのバージョン違いによるメソッドの名前の違いも一応、残しておきます。インテントフィルターの設定と、ブロードキャストレシーバーの登録はフォアグラウンドサービスの方に移したので削除しておきました。

```java:MainActivity.java
package com.example.myapplication;

import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("debug", "onCreate");

        Intent serviceIntent = new Intent(this, ReceivingService.class);
        serviceIntent.putExtra("REQUEST_CODE", 1);

        // Serviceの開始
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(serviceIntent);
        } else {
            startService(serviceIntent);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("debug", "onDestroy");
    }
}
```

実行すると、通知バーにドロイド君の頭のアイコンが表示されます。

![フォアグラウンドサービスの通知.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/28e45b38-b59b-30f1-abfc-7ed4ef44d866.png)

この状態でマルチタスクメニューからアプリを消してもこのアイコンは消えずに、電源接続、切断を検出できました。

## フォアグラウンドサービスの開始と終了
電源接続、切断が検出できた、とは言っても、デバッグ中にPCとの接続ケーブルを抜き差ししたり、電池残量が減ってしまって純粋に充電したいときになどにアプリが何か反応するのも煩わしい時もあるので、そういう時はフォアグラウンドサービスをオプションメニューから終了できるようにしておきたいと思います。

![オプションメニュー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/aa6d28e1-cd39-6303-0c59-82e9d7cb8414.png)


フォアグラウンドサービスの終了は
```java
	Intent serviceIntent = new Intent(getActivity(), ReceivingService.class);
	stopService(serviceIntent);
```
を実行すればよいので、オプションメニューで「自動接続」のチェックを外したら`stopService()`を実行する、という仕組みにしました。ちなみに、これを`MainActivity`の`onDestory()`内に書いてしまうと、アプリが破棄された時点でフォアグラウンドサービスが終了してしまうので、マルチタスクメニューからアプリを消してもケーブルの抜き差しを検出する、という当初の狙いは達成できません．．．。

オプションメニューの設定は`MainActivity`に`onCreateOptionsMenu`と`onOptionsItemSelected`を追加しました。

```java:MainActivity.java

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.option_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.autoStartCheck) {
            item.setChecked(!item.isChecked()); // false -> true, true -> false
            boolean isChecked = item.isChecked();   //  反映後の状態を取得
            if (isChecked) {
                Log.d(TAG, "select autoStart");
                Intent serviceIntent = new Intent(this, ReceivingService.class);
                serviceIntent.putExtra("REQUEST_CODE", 1);

                // Serviceの開始
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    startForegroundService(serviceIntent);
                } else {
                    startService(serviceIntent);
                }
            } else {
                Log.d(TAG, "select stop autoStart");
                // Serviceの終了
                Intent serviceIntent = new Intent(this, ReceivingService.class);
                boolean stopServiceSuccessfully = stopService(serviceIntent);
                if (stopServiceSuccessfully) {
                    Log.d(TAG, "stop ReceivingService Successfully");
                } else {
                    Log.d(TAG, "ReceivingService is not running");
                }
            }
            return true;
        } else {
            Log.d(TAG, "Unexpected value: " + item.getItemId());
            return false;
        }
    }
```

xmlファイルの方は、res/menu ディレクトリを作成し、option_menu.xml として設定しました。

```xml:option_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/autoStart"
        android:title="@string/autoStartSetting"
        app:showAsAction="never" >
        <menu>
            <item
                android:id="@+id/autoStartCheck"
                android:title="@string/autoStartOK"
                android:checkable="true"
                app:showAsAction="never" />
        </menu>
    </item>
</menu>
```
stringは res/values/strings.xml に追加しました。
```xml:strings.xml
<resources>
	...
	
    <!-- Options Menu -->
    <string name="autoStartSetting">自動接続開始設定</string>
    <string name="autoStartOK">自動接続</string>
</resources>
```
![オプションメニューから自動接続設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/dd5c41bf-d14e-5566-497e-864ed6909117.png)

ここで少し気になるのが、チェックボックスのON/OFFは、アプリが生きている間はよさそうですが、マルチタスクメニューでアプリがクリアされる時まで想定すると、そういう時にはON/OFFが初期値に戻ってしまいます。そうすると、例えばONでフォアグラウンドサービスが起動した状態でアプリをクリアすると、次起動したときにフォアグラウンドサービスは引き続き動いているのにチェックボックスがOFFになっている、という状況が起きます。そこで、`SharedPreferences`を使ってみます。「[SharedPreferencesの使い方(基礎編)](https://qiita.com/YAmi/items/3e5640f365277d70f255)」や「[SharedPreferencesの管理方法](https://qiita.com/ryo_mm2d/items/b90dbbd726183c20c14c)」などを参考にさせていただき、Contextさえあれば使えそうなので「[保存されている値を使用する](https://developer.android.com/guide/topics/ui/settings/use-saved-values?hl=ja)」の`PreferenceManager.getDefaultSharedPreferences(Context context)`を使うことにしました。

`MainActivity`に`SharedPreferences`の変数を追加し、`onCreate()`で`PreferenceManager.getDefaultSharedPreferences()`を使えるようにし、`onOptionsItemSelected`でON/OFF選択されたらその結果を`autoStart`というキーで書き込んでおき、`onPrepareOptionsMenu`でオプションメニューが表示される時にSharedPreferencesの設定を読み込んで反映させておく、というようにしておきました。

```java:MainActivity.java

public class MainActivity extends AppCompatActivity{
    ...    
    SharedPreferences sharedPreferences;

    protected void onCreate(Bundle savedInstanceState) {
		...
        sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(this /* Activity context */);
    }
	...

    @Override
    public boolean onPrepareOptionsMenu(@NonNull Menu menu){
        super.onPrepareOptionsMenu(menu);

        boolean isChecked = sharedPreferences.getBoolean(getString(R.string.autoStart), false);
        if(isChecked && !ReceivingService.isRunning()){
            isChecked = false;
        }

        menu.findItem(R.id.autoStart).getSubMenu().findItem(R.id.autoStartCheck).setChecked(isChecked);
        return true;
    }
	...
	
    public boolean onOptionsItemSelected(MenuItem item) {
			...
        if (id == R.id.autoStartCheck) {
 			...			
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putBoolean(getString(R.string.autoStart), isChecked);
            editor.apply();

            return true;
        } else {
 			...
        }
    ...
}
```

# 電源接続に合わせてBluetooth接続、文字列の送信を開始
## Bluetooth接続先の指定
「[【Android】PCとBluetooth接続し、PCに文字列を送信する](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9#%E6%8E%A5%E7%B6%9A%E5%85%88%E3%81%AE%E6%8C%87%E5%AE%9A)」では、毎回、オプションメニューから接続先を指定するようにしていましたが、毎回接続先を指定するのも煩わしいですのでまず、オプションメニューから接続先の設定が確認できる画面(`Activity`)を新設し、その設定が所望の接続先でなければ、接続先設定画面(`DeviceListActivity`)に移行するようにしたいと思います。接続先情報はアプリがクリアされても大丈夫なように`SharedPreferences`に保存し、その新設した画面で`SharedPreferences`の内容の確認と表示、変更あれば保存するようにします。

まず、オプションメニューに、接続先の設定ができる選択肢を追加します。
```xml:option_menu.xml
	...
    <item
        android:id="@+id/deviceSetting"
        android:title="@string/deviceSetting"
        app:showAsAction="never" />
	...
```
```xml:strings.xml
	...
    <!-- Options Menu -->
	...
    <string name="deviceSetting">接続先 設定...</string>
	...
```

さらに、`MainActivity`の`onOptionsItemSelected`にこの選択肢が選択されたときの処理を追加します。`deviceSetting`が選択されたときは新設の`SettingsActivity`を呼び出します。

```java:MainActivity.java
    public boolean onOptionsItemSelected(MenuItem item) {
			...
        if (id == R.id.autoStartCheck) {
 			...			
        } else if(id == R.id.deviceSetting){
            // Ensure this device is discoverable by others
            Log.d(TAG, "deviceSetting");
            Intent serverIntent = new Intent(this, SettingsActivity.class);
            startActivity(serverIntent);
            return true;
        } else {
 			...
        }
    }
}
```

## SettingsActivity
次に、`SettingsActivity`の中身ですが、`MainActivity.java`等を置いていあるディレクトリでFile > New (Windowsの場合は右クリックでも可) > Activity > Setting Activity を選択するとひな形を新設、マニフェストへの登録などしてくれました。これをもとに、適宜修正していきたいと思います。
まず、どんな情報を扱うか、ということですが、`res/xml/root_preferences.xml`が生成されていましたので、ここに定義します。とりあえず、接続先デバイスの名前が分かるように設定しておきます。

```xml:root_preferences.xml
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory app:title="@string/setting_header">

        <Preference
            app:key="@string/deviceName_key"
            app:title="@string/deviceName_title"
            app:summary="@string/deviceName_summary" />

    </PreferenceCategory>

</PreferenceScreen>
```
キーや、画面上に表示されるタイトル、内容も設定しておきます。
```xml:res/values/string
    <!-- root_Preference -->
    <string name="setting_header">接続先 設定</string>
    <string name="deviceName_key">deviceName_key</string>
    <string name="deviceName_title">Device Name</string>
    <string name="deviceName_summary">No Device</string>
```
レイアウトは特に指定なしなので自動生成されたままです。
```xml:res/layout/settings_activity.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/settings"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
```
あともう一つ、res/valuesディレクトリに`arrays.xml`が生成されていましたが、これはどこにも使われていないようなので削除していいと思います。

オプションメニューを選択すると、「自動接続」の下に「接続先設定...」があり、接続先設定を選択すると`SettingsActivity`画面が開きます。

![OptionMenu_接続先.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/75062876-55c7-869d-e68c-512208adedcf.png)

最初は「Device Name」のところが「No Device」になっていますが、タップすると`DeviceListActivity`が開始されます。

![DeviceName_NoDevice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/9819e2fd-6a89-286c-727a-af7cad421590.png)

すると、デバイスに設定されているBluetoothデバイスが一覧で出てくるので、選択します。

![接続先選択画面_Zoom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/2676f1b0-2099-ee5a-b2a1-ed0bc38451f4.png)


「My PC」を選択した場合は、こんな表示になります。

![DeviceName_MyPC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/9a997b5f-24e3-4c49-6acb-b3266a4856d7.png)


以上の動きを実現しているソースコードは以下のようになります。

`SettingsFragment`の`onCreatePreferences`メソッド内で`root_preferences.xml`を指定し、`findPreference(getString(R.string.deviceName_key))`の`setOnPreferenceClickListener`に画面上をタップされたときの処理(`DeviceListActivity`の開始)を設定します。ここはオリジナルでは`startActivityForResult`を使って、`onActivityResult`で戻ってきた値を受け取っていますが、使えなくなったようなので、`ActivityResultLauncher`を使った方法にしています。

`DeviceListActivity`は「[【Android】PCとBluetooth接続し、PCに文字列を送信する](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9)」の[DeviceListActivity.java](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9#devicelistactivityjava)を持ってきます。

戻ってきたBluetooth デバイスの名前とアドレスは、`Bundle`の`getString`を使って取り出し、`SettingsActivity`画面の方も`setSummary`で変更し、同時に`SharedPreferences`にも書き込んでおきます。

```java:SettingsActivity.java
package com.example.myapplication;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        if (savedInstanceState == null) {
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.settings, new SettingsFragment())
                    .commit();
        }
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            // Respond to the action bar's Up/Home button
            case android.R.id.home:
                super.onBackPressed();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public static class SettingsFragment extends PreferenceFragmentCompat {
        Context context = MainActivity.getInstance();
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey);

            Preference deviceName = findPreference(getString(R.string.deviceName_key));
            String mDeviceName = sharedPreferences.getString(getString(R.string.SharedPreferenceDeviceName), "");
            if (deviceName != null) {
                if(mDeviceName.length() > 0){
                    deviceName.setSummary(mDeviceName);
                }
                deviceName.setOnPreferenceClickListener(preference -> {
                    Log.d("SettingsFragment", "on deviceName");
                    Intent intent = new Intent(getContext(), DeviceListActivity.class);
                    mGetBluetoothAddress.launch(intent);
                    return true;
                });
            }
        }
        ActivityResultLauncher<Intent> mGetBluetoothAddress = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
                result -> {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        if (result.getData() != null) {
                            Bundle extras = result.getData().getExtras();
                            String deviceName = extras.getString(DeviceListActivity.EXTRA_DEVICE_NAME);
                            String address = extras.getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
                            Log.d("mGetBluetoothAddress.getData ", "deviceName = " + deviceName + ",address = " + address);
                            Preference preferenceDeviceName = findPreference(getString(R.string.deviceName_key));
                            if (preferenceDeviceName != null) {
                                preferenceDeviceName.setSummary(deviceName);
                            }

                            SharedPreferences.Editor editor = sharedPreferences.edit();
                            editor.putString(getString(R.string.SharedPreferenceDeviceName), deviceName);
                            editor.putString(getString(R.string.SharedPreferenceDeviceAddress), address);
                            editor.apply();

                        }
                    }
                });
    }
}
```

Android Studio の File → New → Activity でファイルを作ると自動的にマニフェストの`activity`の設定を自動で追加してくれたのですが、自動で作っていない場合は設定を追加が必要です。
```xml:AndroidManifest.xml
	...
        <activity
            android:name=".DeviceListActivity"
            android:exported="false"
            android:label="@string/select_device"
            android:theme="@android:style/Theme.Holo.Dialog" />
        <activity
            android:name=".SettingsActivity"
            android:exported="false"
            android:label="@string/title_activity_settings" />
    ...
```

あれ、`android:exported="false"`ってなに？と思ったら「[android:exported='false' とはどういうことか？](https://qiita.com/ryo_mm2d/items/b85cc6f3778b540f9ada)」という記事がありました。アクティビティを他のアプリのコンポーネントから起動できるかどうか、起動できない(させたくない)場合は 'false' でいいそうです。

### 小話(1) `DeviceListActivity` と `Theme`
`DeviceListActivity`は元々、「[android-BluetoothChat](https://github.com/googlesamples/android-BluetoothChat)」から持ってきていますが、File > New > Activity > Empty Activity でクラスを作って必要部分をコピーし、`res/layout/activity_device_list.xml`と`res/layout/device_name.xml`をコピーして試しました。すると、以前は先程の画面のように選択するリストが画面中央に浮き上がったように表示されたのですが、今回は様子が違いました。何が違うか比較すると、マニフェストに自動で`<activity ～`が挿入されていて、
```xml:AndroidManifest.xml
	...
        <activity
            android:name=".DeviceListActivity"
            android:exported="false" />
	...
```
となっているのに対して、`android-BluetoothChat`のマニフェストは
```xml:android-BluetoothChatのAndroidManifest.xml
	...
        <activity
            android:name=".DeviceListActivity"
            android:configChanges="orientation|keyboardHidden"
            android:label="@string/select_device"
            android:theme="@android:style/Theme.Holo.Dialog"/>
	...
```
となっていて、そうか、`theme`が違うのか、ということでここもコピーして試しました。すると、

	java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.myapplication/com.example.myapplication.DeviceListActivity}: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

というエラーが出て落ちてしまいます。Theme.AppCompatと言われても良く分からなったので調べたところ、「[Android : Theme.Holo VS Theme.AppCompat](https://stackoverflow.com/questions/36838033/android-theme-holo-vs-theme-appcompat)」 に
```
<application
    android:theme="@style/Theme.AppCompat"
    ...
```
と書くんだよ、と書いてあったのでこれで試してみました。すると、エラーは出なくなったのですが、表示されるイメージがちょっと違います。

「[AppCompat widget that can only be used with a Theme.AppCompat theme (or descendant)](https://stackoverflow.com/questions/66968817/appcompat-widget-that-can-only-be-used-with-a-theme-appcompat-theme-or-descenda)」に、`Activity`なんとか、と書いてあったのでソースコードをよく見ると、[android-BluetoothChat](https://github.com/googlesamples/android-BluetoothChat) の方は
```java:android-BluetoothChatのDeviceListActivity.java
public class DeviceListActivity extends Activity 
```

になっているのに対して、今回作った方は

``` java:DeviceListActivity.java
public class DeviceListActivity extends AppCompatActivity
```
になっていました。今回たまたま、File > New > Activity > Empty Activity でクラスを作って必要部分をコピーしたのですが、そうすると、`AppCompatActivity`が継承されたクラスになっていたようです。`Activity`に修正すると`Theme.Holo.Dialog`でもエラーなくビルド出来て、元の画面表示がでることがわかりました。見た目だけの問題なので、どちらでもいいのですが、一方で、R.style | Android Developers の「[Theme_Holo_Dialog](https://developer.android.com/reference/android/R.style.html#Theme_Holo_Dialog)」にAPI 28から非推奨で他のAPI 21以上のテーマを使うか、AppCompat をサポートしているAPIと使うように、というようなメッセージも書いてあるので、いずれ使えなくなるのかもしれませんね。Themeはあまり詳しくありませんので、また別の機会に調べたいと思います。

![接続先選択画面_Themeによる差.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/dd0ab23b-1b44-a2f8-bfa2-e40404010649.png)

![Theme_Holo_Dialog deprecated.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/a56d4802-6d8b-8e98-176e-7960d464f4e2.png)


### 小話(2) アクションバーの「←」

![SettingsActivityのback.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2335076/a9f466a0-b4f7-b608-9408-dc6ebb3619cc.png)

自動で生成された時点で、
```java
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }
```
となっていて、アクションバー(画面の一番上)に「←」が表示されているのですが、`onOptionsItemSelected`を実装しないと押しても動かないです([Navigate back from settings activity](https://stackoverflow.com/questions/34222591/navigate-back-from-settings-activity)）。自動で生成するなら、これも入れておいてくれればいいのに、と思いました。

```java
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            // Respond to the action bar's Up/Home button
            case android.R.id.home:
                super.onBackPressed();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
```
## Bluetooth接続処理の呼び出し
これまでの`PowerConnectionReceiver`と名付けた`BroadcastReceiver`では、電源の接続検出してもToastを出すだけなので、接続に合わせて何か処理をするには、`BroadcastReceiver`を呼び出した(`ReceivingService`と名付けたフォアグラウンドサービス)側に、接続されたことを伝える必要があります。ということで、今回は「[[Android] カスタム Listener を interface を使って実装してみる](https://akira-watson.com/android/custom-listener.html)」を参考に、`BroadcastReceiver`に`interface`を追加しました。電源接続の確立通知として`onPowerConnected()`、失敗時の`onPowerConnectFailed()`、切断の`onPowerDisconnected()`と名付け、`PowerConnectionReceiver`を呼び出したときに、どこにこれらの実態が実装されているかを設定してもらうようにします。で、いままでToastを出していたところに、これらのメソッドを追加しておきます。

```java:PowerConnectionReceiver.java
public class PowerConnectionReceiver extends BroadcastReceiver {

    public interface connectionInterface{
        /** Power接続の確立通知を受けるメソッド */
        void onPowerConnected();
        /** Power接続失敗の通知を受けるメソッド */
        void onPowerConnectFailed();
        /** Power接続の切断通知を受けるメソッド */
        void onPowerDisconnected();
    }

    private connectionInterface connectionListener = null;

    PowerConnectionReceiver(connectionInterface listener){
        connectionListener = listener;
    }

public class PowerConnectionReceiver extends BroadcastReceiver {
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getSimpleName() ;

    public interface connectionInterface{
        /** Power接続の確立通知を受けるメソッド */
        void onPowerConnected();
        /** Power接続失敗の通知を受けるメソッド */
        void onPowerConnectFailed();
        /** Power接続の切断通知を受けるメソッド */
        void onPowerDisconnected();
    }

    private connectionInterface connectionListener = null;

    private static int mPluggedLast = -1;

    PowerConnectionReceiver(connectionInterface listener){
        connectionListener = listener;
    }    
    ...
    
    @Override
    public void onReceive(Context context, Intent intent) {
		...		
                if (nowPlugged == BatteryManager.BATTERY_PLUGGED_AC) {        // int android.os.BatteryManager.BATTERY_PLUGGED_AC : 1 [0x1]
                    Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_AC");
                    Toast.makeText(context, "BATTERY_PLUGGED_AC", Toast.LENGTH_LONG ).show();
                    if(connectionListener != null){
                        connectionListener.onPowerConnected();
                    }
                } else if (nowPlugged == BatteryManager.BATTERY_PLUGGED_USB) {
                    //	int android.os.BatteryManager.BATTERY_PLUGGED_USB : 2 [0x2]
                    Log.d(TAG, "ケーブルの接続状態 = BATTERY_PLUGGED_USB");
                    Toast.makeText(context, "BATTERY_PLUGGED_USB", Toast.LENGTH_LONG ).show();
                    if(connectionListener != null){
                        connectionListener.onPowerConnected();
                    }
                } else if (nowPlugged == 0) {
                    Log.d(TAG, "Unplugged.");
                    Toast.makeText(context, "Unplugged.", Toast.LENGTH_LONG ).show();
                    if(connectionListener != null){
                        connectionListener.onPowerDisconnected();
                    }
                } else {               
                ...                
                }
		...
}	

```
次に、`PowerConnectionReceiver`は`ReceivingService`と名付けたフォアグラウンドサービスで呼び出されるので、`ReceivingService`が`PowerConnectionReceiver.connectionInterface`を`implements`し、`ReceivingService`に`onPowerConnected()`、`onPowerConnectFailed()`、`onPowerDisconnected()`でどうするか記述します。`onPowerConnected()`が呼ばれたときは、そこから`MeasureService`というこれまたフォアグラウンドサービスを呼び出し、ようやく電源接続して自動で開始したい処理が開始できます。接続に失敗したり、切断されたときは`MeasureService`を停止するようにしておきます。

```java:ReceivingService.java
public class ReceivingService extends Service implements PowerConnectionReceiver.connectionInterface{
...
    @Override
    public void onPowerConnected() {
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */);
        boolean isChecked = sharedPreferences.getBoolean(getString(R.string.autoStart), false);
        if(isChecked){
            if(!MeasureService.isRunning()){
                Intent serviceIntent = new Intent(this, MeasureService.class);
                serviceIntent.putExtra("REQUEST_CODE", 1);

                // Serviceの開始
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    startForegroundService(serviceIntent);
                } else {
                    startService(serviceIntent);
                }
            } else {
                Log.d(TAG, "MeasureService.isRunning");
            }
        }
    }

    @Override
    public void onPowerConnectFailed() {
        Intent serviceIntent = new Intent(getApplication(), MeasureService.class);
        stopService(serviceIntent);
    }

    @Override
    public void onPowerDisconnected() {
        Intent serviceIntent = new Intent(getApplication(), MeasureService.class);
        stopService(serviceIntent);
    }
}
```

`MeasureService`もまず、Bluetoothが接続されたときの通知が受け取れるように`Intereface`を`implements`しておきます。そのあとはフォアグラウンドサービスとして実施することは同じで、このサービスが開始されたら`Notification Channel`等を設定し、Bluetooth接続処理を実施しています。接続出来たら`onBluetoothConnected()`の処理を実施、接続できなかったら、接続できなかった時のメッセージをインテントに載せて送信、接続処理を終了します(`onBluetoothConnectFailed()`)。

```java:MeasureService.java
package com.example.myapplication;

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.content.SharedPreferences;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.util.Log;

import java.util.Objects;

public class MeasureService extends Service implements BluetoothConnection.connectionInterface{
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getSimpleName() ;

    MeasureThread thread;
    NotificationManager notificationManager;
    String channelId;
    String title;

    private static boolean onMeasure;
    private BluetoothConnection mBluetoothConnection;

    public static boolean isRunning(){return onMeasure;}

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

    public int onStartCommand(Intent intent, int flags, int startId) {
        String methodName = new Object(){}.getClass().getEnclosingMethod().getName() ;
        Log.d(TAG, methodName);
        Log.d(TAG, "Thread name = " + Thread.currentThread().getName());

        onMeasure = true;

        int requestCode = intent.getIntExtra("REQUEST_CODE",0);
        Context context = getApplicationContext();
        channelId = "default";
        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) 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.mipmap.sym_def_app_icon)
                        .setContentText("車両情報取得中")
                        .setAutoCancel(true)
                        .setContentIntent(pendingIntent)
                        .setWhen(System.currentTimeMillis())
                        .build();

                // startForeground
                startForeground(1, notification);

            }
        }

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context /* Activity context */);
        String address = sharedPreferences.getString(getString(R.string.SharedPreferenceDeviceAddress), "");

        if(address.length() > 0){
            connectDevice(address);
        }

//        stopSelf();
        return START_NOT_STICKY;
    }
    @Override
    public void onDestroy() {
        super.onDestroy();

        String methodName = new Object(){}.getClass().getEnclosingMethod().getName() ;
        Log.d(TAG, methodName);

        if (thread != null) thread.cancel();
        onMeasure = false;

        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */);
        boolean isChecked = sharedPreferences.getBoolean(getString(R.string.autoStart), false);
        if(isChecked) {
            Log.d(TAG + "." + methodName, getString(R.string.waiting));
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                Notification notification = new Notification.Builder(this, channelId)
                        .setContentTitle(title)
                        .setContentText(getString(R.string.waiting))
                        .setSmallIcon(android.R.mipmap.sym_def_app_icon)
                        //		.setSmallIcon(android.R.drawable.sym_def_app_icon)
                        .setContentText(getString(R.string.waiting))
                        .setAutoCancel(true)
                        .setWhen(System.currentTimeMillis())
                        .build();
                notificationManager.notify(1, notification);
                //}
            }
        } else {
            Log.d(TAG + "." + methodName, "notification cancel");
            notificationManager.cancel(1);
        }
    }

    private void connectDevice(String address){
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */);
        String mDeviceName = sharedPreferences.getString(getString(R.string.SharedPreferenceDeviceName), "");
        Log.d(TAG, "name = " + mDeviceName + ",address = " + address);
        // Get the BluetoothDevice object
//        mBluetoothConnection = BluetoothConnection.getInstance(address);
        mBluetoothConnection = new BluetoothConnection(this, address);
        mBluetoothConnection.setConnectionListener(this);
        mBluetoothConnection.connect();
    }
    @Override
    public void onBluetoothConnected() {
        Intent broadcast = new Intent();
        broadcast.putExtra("onBluetoothConnected", true);
        broadcast.setAction("DO_ACTION");
        getBaseContext().sendBroadcast(broadcast);

        thread = new MeasureThread(mBluetoothConnection);
        thread.start();
        // memo
    }

    @Override
    public void onBluetoothConnectFailed() {
        Intent broadcast = new Intent();
        broadcast.putExtra("onBluetoothConnectFailed", true);
        broadcast.setAction("DO_ACTION");
        getBaseContext().sendBroadcast(broadcast);

        if (mBluetoothConnection != null) {
            mBluetoothConnection.close();
            Log.d(TAG + ".onBluetoothConnectFailed", "mBluetoothConnection.close()");
            mBluetoothConnection = null;
        }
    }
}
```

## Bluetooth接続後の処理
Bluetooth接続後の処理として`MeasureThread`というクラスを別スレッドで起動できるように`Thread`を継承してつくりました。まずはPCでの実験用に、文字列を送信する処理で試しました。これは「【Android】PCとBluetooth接続し、PCに文字列を送信する」の[`MyService.java`](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9#myservicejava)にあった、内部クラスの`private class ConnectedThread extends Thread`と同じです。別のファイルにすることで、最終的に実施したい、OBD2アダプターを使ったデータ計測と簡単に差し替えられるようにしました。
```java:MeasureThread.java
package com.example.myapplication;

import android.util.Log;

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

public class MeasureThread extends Thread {
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getSimpleName() ;
    boolean onActive = true;

    OutputStream out;

    public MeasureThread(BluetoothConnection bluetoothConnection) {
        Log.d("MeasureThread", "Thread name = "
                + Thread.currentThread().getName());
        out = Objects.requireNonNull(bluetoothConnection.getOutputStream());
    }

    public void run() {
        Log.d("MeasureThread 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();
        }
    }
    public void cancel(){
        onActive = false;
        Log.d("MeasureThread", "cancel");
    }
}
```

## PCでの確認
実機でバックで動作確認しました。基本的なやり方は前回の「[Bluetooth接続の実機テスト](https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9#bluetooth%E6%8E%A5%E7%B6%9A%E3%81%AE%E5%AE%9F%E6%A9%9F%E3%83%86%E3%82%B9%E3%83%88)」と同様ですが、違うところは、Teratermを立ち上げておき、オプションメニューから接続先を設定し、同じくオプションメニューにある「自動接続」をオンにすると、電源接続判定からBluetooth接続、送信処理が自動で実行されるのでデバイスの画面のボタンを押さなくても、Teraterm画面に文字が表示されました。

# OBD2アダブターとの送受信機能を追加
「[AndroidスマホとOBD2アダプターを使って車の内部情報を取得するのに「obd2-lib」を活用](https://qiita.com/naoto_deuxchats/items/33564ad8eec4ad310041)」を参考に、`commands`ディレクトリ、内のファイル`IntegerArrayStream.java`、`Obd2Command.java`、`Obd2Connection.java`、`Obd2LiveFrameGenerator.java`を持ってきます。さらに、`BluetoothConnection`クラスに`implements Obd2Connection.UnderlyingTransport`を追加、`MeasureThread`を以下のように変更します。
```java:MeasureThread.java
package com.example.myapplication;

import android.content.Context;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Objects;

public class MeasureThread extends Thread {
    public final String TAG = Objects.requireNonNull(new Object() {}.getClass().getEnclosingClass()).getSimpleName() ;
    boolean onActive = true;

    BluetoothConnection mBluetoothConnection;
    Obd2Connection mObd2Connection;
    Obd2LiveFrameGenerator mObd2LiveFrameGenerator;
    Context context;

    public MeasureThread(Context _context, BluetoothConnection bluetoothConnection) {
        Log.d("MeasureThread", "Thread name = "
                + Thread.currentThread().getName());
        mBluetoothConnection = bluetoothConnection;
        context = _context;
    }

    public void run() {
        Log.d("MeasureThread run", "Thread name = "
                + Thread.currentThread().getName());
        mObd2Connection = new Obd2Connection(mBluetoothConnection);
        try {
            mObd2LiveFrameGenerator = new Obd2LiveFrameGenerator(mObd2Connection);
            File myDataFile = new File(context.getFilesDir(), "test.csv");
            FileWriter myDataFileWriter = new FileWriter(myDataFile);
            boolean needTitle = true;
            long startTimeNano = 0;
            while (onActive) {
                JSONObject jsonObject = mObd2LiveFrameGenerator.generate();
                Log.d("ConnectedThread run", "jsonObject = " + jsonObject.toString());

                long timestamp = jsonObject.getLong("timestamp");
                if(startTimeNano == 0){
                    startTimeNano = timestamp;
                }
                long elapsedTimeNano = timestamp - startTimeNano;
                JSONArray datas = jsonObject.getJSONArray("data");

                StringBuilder titles = new StringBuilder();
                StringBuilder values = new StringBuilder();
                titles.append("Elapsed Time");
                values.append(elapsedTimeNano / 1.0e9);   //[sec]

                if(datas.length() > 0){
                    for(int j = 0; j < datas.length(); j++){
                        JSONObject data = datas.getJSONObject(j);
                        titles.append(",").append(data.get("id"));
                        values.append(",").append(data.get("value"));
                    }

                    if(needTitle){
                        myDataFileWriter.write(titles + "\r\n");
                        needTitle = false;
                    }
                    myDataFileWriter.write(values + "\r\n");
                }
            }
            myDataFileWriter.close();

        } catch (IOException | InterruptedException | JSONException e) {
            e.printStackTrace();
            onActive = false;
            Log.d(getClass().getSimpleName(), "onBluetoothDisconnected");
            e.printStackTrace();
        }
        //  正常終了時   異常時はonBluetoothDisconnected()の方が早い→mBluetoothConnection = nullの可能性もあり
        if (mBluetoothConnection != null) {
            mBluetoothConnection.close();
            Log.d("ConnectedThread", "mBluetoothConnection.close()");
            mBluetoothConnection = null;
        }

        Log.d("ConnectedThread", "end");
    }
    public void cancel(){
        onActive = false;
        Log.d("MeasureThread", "cancel");
    }
}
```
取得したデータを書き出す場所を取得する`getFilesDir()`を使う関係で、`Context`を引数に追加しています。ので、呼び出し側の`MeasureService`も`Context`を追加で渡すように変更しています。

```java:MeasureService.java

    @Override
    public void onBluetoothConnected() {
        ...
        thread = new MeasureThread(this, mBluetoothConnection);
        ...
	}
```

以上で「[AndroidスマホとOBD2アダプターを使って車の内部情報を取得するのに「obd2-lib」を活用](https://qiita.com/naoto_deuxchats/items/33564ad8eec4ad310041)」にデバイスと車をUSBケーブルで接続しておき、車のイグニッションをＯＮにする(スタートボタンを押す)と同時に計測開始をする機能が追加できました。実際に試してみると、高速道路でSA/PAで休憩し、走行再開するような状況でも、毎回アプリのスタートボタンを押さなくても計測できるので便利にはなったのですが、やはり100%自動にはならず、夏の車内にデバイスを置いていたような状況では「バッテリー温度が高温です」というメッセージと共にForgroundServiceが勝手に終了になっていたりはします。そういう状況でデバイス(スマホ)を使用するのもスマホのバッテリー寿命の為にも悪い気もしますので、長時間放置する可能性があるときは一度USBケーブルを抜いて持ち歩く等、使い方でカバーするようにします。

# 参考にした記事等

https://qiita.com/naoto_deuxchats/items/33564ad8eec4ad310041

https://developer.android.com/training/monitoring-device-state/battery-monitoring?hl=ja

http://pdfandfont.blogspot.com/2020/06/android.html

https://developer.android.com/guide/components/broadcasts?hl=ja

https://qiita.com/naoto_deuxchats/items/447d609166bc639c9436

https://qiita.com/YAmi/items/3e5640f365277d70f255

https://qiita.com/ryo_mm2d/items/b90dbbd726183c20c14c

https://developer.android.com/guide/topics/ui/settings/use-saved-values?hl=ja

https://qiita.com/naoto_deuxchats/items/e6a9af6c7a9ee40f0fb9

https://qiita.com/ryo_mm2d/items/b85cc6f3778b540f9ada

https://github.com/googlesamples/android-BluetoothChat

https://stackoverflow.com/questions/36838033/android-theme-holo-vs-theme-appcompat

https://stackoverflow.com/questions/66968817/appcompat-widget-that-can-only-be-used-with-a-theme-appcompat-theme-or-descenda

https://developer.android.com/reference/android/R.style.html#Theme_Holo_Dialog

https://stackoverflow.com/questions/34222591/navigate-back-from-settings-activity

https://akira-watson.com/android/custom-listener.html