Android で iBeacon を受信

以前 teratail の質問で iBeacon を Android で受信する関連があり、 AltBeacon を使って範囲内の iBeacon のリストを表示するアプリを試した所動いたので回答しました。
ちょっと良い感じに出来たと思ったので、改造( ListView を RecyclerView にしたり細々)してこちらにも載せておきます。
元々質問者さんは『Beaconアプリ開発記【その1　開発準備編】』( https://houwa-js.co.jp/2018/08/20180808/ ) を参考にされていたそうなのでこのプログラムも参考にさせて頂いています。
AltBeacon に関する処理は全て settingAltBeacon メソッドにあるので、他のライブラリを使う場合はそこだけ変えればと想定しています。

環境:
・リュウド株式会社 ReUdo R1Beacon (RE-BC-BLE401W-1) BluetoothLE4.0
・SONY Xperia XZ3 (au) Android10, Bluetooth5.0
・altbeacon 2.20.6

libs.versions.toml に追加
```
[versions]
:(略)
androidBeaconLibrary = "2.20.6"

[libraries]
:(略)
android-beacon-library = { group = "org.altbeacon", name = "android-beacon-library", version.ref = "androidBeaconLibrary" }
```
build.gradle.kts(:app) に追加
```
dependencies {
  :(略)
  implementation(libs.android.beacon.library)
}
```
MainActivity.java
```java
import static androidx.activity.result.contract.ActivityResultContracts.*;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.*;
import android.util.Log;
import android.view.*;
import android.widget.*;

import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.*;
import androidx.appcompat.app.*;
import androidx.lifecycle.*;
import androidx.recyclerview.widget.RecyclerView;

import org.altbeacon.beacon.*;

import java.util.*;
import java.util.stream.Collectors;

public class MainActivity extends AppCompatActivity {
  private static final String LOG_TAG = "MainActivity";

  private final ActivityResultLauncher<String[]> launcher =
          registerForActivityResult(new RequestMultiplePermissions(), granted -> {
            if(granted.get(Manifest.permission.ACCESS_FINE_LOCATION) == Boolean.TRUE) {
              start();
            } else {
              finish();
            }
          });

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

    if(checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
      start();
    } else {
      launcher.launch(new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
    }
  }

  private void start() {
    TextView resultText = findViewById(R.id.result);
    RecyclerView recyclerView = findViewById(R.id.beacon_list);

    BeaconAdapter adapter = new BeaconAdapter();
    recyclerView.setAdapter(adapter);

    adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
      @Override
      public void onChanged() {
        resultText.setText(String.format(Locale.getDefault(), "検知Beacon数： %d 台", adapter.getItemCount()));
      }
    });

    settingAltBeacon(adapter);
  }

  private static final String BEACON_LAYOUT = "m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24";

  private void settingAltBeacon(BeaconAdapter adapter) {
    BeaconManager beaconManager = BeaconManager.getInstanceForApplication(this);
    beaconManager.getBeaconParsers().add(new BeaconParser().setBeaconLayout(BEACON_LAYOUT));

    getLifecycle().addObserver(new DefaultLifecycleObserver() {
      private final Region region = new Region("iBeacon", null, null, null);
      @Override
      public void onResume(@NonNull LifecycleOwner owner) {
        beaconManager.startRangingBeacons(region);
      }
      @Override
      public void onPause(@NonNull LifecycleOwner owner) {
        beaconManager.stopRangingBeacons(region);
      }
    });

    beaconManager.addRangeNotifier((beacons, region) -> {
      Log.d(LOG_TAG, "InRegion " + beacons.size());
      List<BeaconItem> list = beacons.stream()
              .map(beacon ->
                new BeaconItem(beacon.getId1().toUuid(), beacon.getId2().toInt(), beacon.getId3().toInt(),
                               beacon.getRssi(), beacon.getTxPower(), beacon.getDistance()))
              .sorted(Comparator.comparingDouble(a -> a.distance))
              .collect(Collectors.toList());
      adapter.set(list);
    });
  }

  private static class BeaconAdapter extends RecyclerView.Adapter<BeaconAdapter.ViewHolder> {
    private List<BeaconItem> itemList = Collections.emptyList();

    void set(@NonNull List<BeaconItem> list) {
      itemList = new ArrayList<>(list); //防御コピー
      notifyDataSetChanged();
    }

    @Override
    public int getItemCount() {
      return itemList.size();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
      return new ViewHolder(parent);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
      holder.bind(itemList.get(position));
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
      private static View inflate(@LayoutRes int resource, @NonNull ViewGroup parent) {
        return LayoutInflater.from(parent.getContext()).inflate(resource, parent, false);
      }

      private final TextView beacon;

      ViewHolder(ViewGroup parent) {
        super(inflate(R.layout.beacon_view, parent));
        beacon = itemView.findViewById(R.id.beacon);
      }

      void bind(BeaconItem item) {
        beacon.setText(String.format(Locale.getDefault(),
                "UUID:%s\nmajor:%d minor:%d\nRSSI:%d TxPower:%d Distance:%f",
                item.uuid.toString(), item.major, item.minor,
                item.rssi, item.txPower, item.distance));
      }
    }
  }

  private static class BeaconItem {
    final UUID uuid;
    final int major, minor, rssi, txPower;
    final double distance;

    BeaconItem(UUID uuid, int major, int minor, int rssi, int txPower, double distance) {
      this.uuid = uuid;
      this.major = major;
      this.minor = minor;
      this.rssi = rssi;
      this.txPower = txPower;
      this.distance = distance;
    }
  }
}
```
AndroidManifest.xml への追加
```xml
  <uses-permission android:name="android.permission.BLUETOOTH" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

  <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
```
layout/activity_main.xml
```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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

  <TextView
      android:id="@+id/result"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:text="検知Beacon数： 0 台"
      android:textSize="18sp"
      android:background="#e0e0e0"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/beacon_list"
      android:layout_width="0dp"
      android:layout_height="0dp"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/result" />

</androidx.constraintlayout.widget.ConstraintLayout>
```
layout/beacon_view.xml
```xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/beacon"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="16sp" />
```