AndroidでバックグラウンドでUDP受信を待ち受ける

Androidスマホにおいて、常にUDPパケット受信を待ち受けるバックグラウンドサービスを作成します。
UDPパケットをAndroidのフォアグラウンドサービスとして常に待機状態とし、受信したUDPパケットの内容に応じて、処理をして、また待ち受けます。
できる処理は多くはないですが、今回は、トースト表示とNotification通知をします。

ソースコードもろもろをGitHubに上げておきました。

poruruba/UdpBackgroundService

https://github.com/poruruba/UdpBackgroundService

# フォアグラウンドサービスの立ち上げ

フォアグラウンドサービスの立ち上げには、MainActivityから以下を実行します。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\MainActivity.java
                Intent intent;
                intent = new Intent(this, UdpBackgroundService.class);
                intent.putExtra("udpPort", 1234);
                startForegroundService(intent);
```

待ち受けるポート番号を渡してあげるようにしました。

詳細は以下をご参照ください。
　[M5StickCのボタン押下でスマホから現在地をしゃべらせる(1/2)](https://qiita.com/poruruba/items/cedc91f9728006d1ba46)

停止は以下です。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\MainActivity.java
                Intent intent = new Intent(this, UdpBackgroundService.class);
                stopService(intent);
```

# フォアグラウンドサービスの実装

Serviceを派生させたクラスとして実装します。以下のメソッドが大事です。

・onCreate
　このクラスが生成されたときに呼ばれます。最初の共通処理として使います。
・onStartCommand
　フォアグラウンドサービスとして開始するときに呼び出されます。
・onDestroy
　フォアグラウンドサービスの停止を要求されたときに呼び出されます。

## onCreate

Notificationのチャネルを設定します。フォアグラウンドサービスとして立ち上げるときには必須です。2つのチャネルを設定しますが、1つ目が必須の方です。後者は必須ではないですが、UDPパケットを受信したときにする処理として使います。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\UdpBackgroundService.java
        Context context = getApplicationContext();
        notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, NOTIFICATION_TITLE , NotificationManager.IMPORTANCE_DEFAULT);
        // 通知音を消さないと毎回通知音が出てしまう
        // この辺りの設定はcleanにしてから変更
        channel.setSound(null,null);
        channel.enableVibration(false);
        notificationManager.createNotificationChannel(channel);

        NotificationChannel channel2 = new NotificationChannel(CHANNEL_ID2, NOTIFICATION_MESSAGE , NotificationManager.IMPORTANCE_DEFAULT);
        notificationManager.createNotificationChannel(channel2);
```

## onStartCommand

フォアグラウンドサービスとして開始するときに呼び出されます。ここが一番大事です。
やっていることは、UDPの準備をしてからノーティフィケーションをしています。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\UdpBackgroundService.java
        int udpPort = intent.getIntExtra("udpPort", MainActivity.DEFAULT_UDP_PORT);
        Log.d(TAG, "udpPort: " + udpPort);
        try{
            if( udpReceive != null ) {
                udpReceive.close();
                udpReceive = null;
            }
            udpReceive = new DatagramSocket(udpPort);
        }catch(Exception ex){
            Log.d(TAG, ex.getMessage());
        }

        force_stop = false;
        Thread thread = new Thread(new UdpReceiveThread());
        thread.start();

        Intent notifyIntent = new Intent(this, MainActivity.class);
//        notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

        Notification notification = new Notification.Builder(getApplicationContext(), CHANNEL_ID)
                .setContentTitle(NOTIFICATION_TITLE)
                .setSmallIcon(android.R.drawable.btn_star)
                .setContentText(NOTIFICATION_CONTENT)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .setWhen(System.currentTimeMillis())
                .build();
        startForeground(NOTIFICATION_ID, notification);

        return START_NOT_STICKY;
```

## UDP受信処理

UDP受信処理について、補足します。
UDP受信はネットワーク処理なので、メインスレッドとは別のスレッドで実行する必要があります。そのため、Runnableを派生させたスレッドクラスを作成しました。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\UdpBackgroundService.java
    private class UdpReceiveThread implements Runnable{
        @Override
        public void run() {
            Log.d(TAG, "UdpReceiveThread run");

            try {
                byte[] buff = new byte[UDP_BUFFER_SIZE];
                while(!force_stop) {
                    Arrays.fill(buff, (byte)0);
                    DatagramPacket packet = new DatagramPacket(buff, buff.length);
                    udpReceive.setSoTimeout(0);
                    udpReceive.receive(packet);
                    Log.d(TAG, "received from " + packet.getAddress());

                    try {
                        JSONObject json = new JSONObject(new String(buff));
                        String type = "";
                        try{
                            type = json.getString("type");
                        }catch(Exception ex){
                            Log.d(TAG, ex.getMessage());
                        }
                        switch(type){
                            case "toast":{
                                Handler mainHandler = new Handler(getMainLooper());
                                mainHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            String message = json.getString("message");
                                            Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
                                        } catch (Exception e) {
                                            e.printStackTrace();
                                        }
                                    }
                                });
                                break;
                            }
                            case "notification":{
                                String title = json.getString("title");
                                String message = json.getString("message");

                                NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID2);
                                builder.setContentTitle(title);
                                builder.setContentText(message);
                                builder.setSmallIcon(android.R.drawable.ic_popup_reminder);
                                builder.setAutoCancel(true);
                                notificationManager.notify(0, builder.build());
                                break;
                            }
                        }
                    }catch(Exception ex2){
                        Log.d(TAG, ex2.getMessage());
                    }
                }
            } catch (Exception ex) {
                Log.d(TAG, ex.getMessage());
            } finally {
                Log.d(TAG, "UdpReceiveThread end");
            }
        }
    }
```

UDPパケットの受信ポート番号を指定してインスタンス化した後、ひたすら待ち受けて、受信したら処理をして、また待ち受ける、を繰り返しています。
UDPパケットは、JSON形式であることを想定しています。今回は以下の2種類です。

```
・トースト表示
type: "toast",
message: ”任意のメッセージ"

・ノーティフィケーション
type: "notification",
title: "任意のタイトル",
message: "任意のメッセージ"
```

## onDestroy

フォアグラウンドサービスを停止します。force_stopをtrueにして受信ループを抜け出させます。

```java:Android\BackgroundUdpServer\app\src\main\java\jp\or\myhome\sample\backgroundudpserver\UdpBackgroundService.java
        force_stop = true;
        if( udpReceive != null ) {
            udpReceive.close();
            udpReceive = null;
        }
```

## AndroidManifest.xml

以下のサービスの設定をapplicationのところに追記します。

```xml:Android\BackgroundUdpServer\app\src\main\AndroidManifest.xml
        <service
            android:name=".UdpBackgroundService"
            android:parentActivityName=".MainActivity"
            android:enabled="true"
            android:exported="true"></service>
```

また、必要なパーミッションがありますので、それも追記しておきます。それぞれ、フォアグラウンドサービスのためと、WiFiのIPアドレスの取得のための、UDPパケット受信のためです。

```xml:Android\BackgroundUdpServer\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
```

# UDP送信のためのWebページ

参考までに、UDP送信のためのWebページも用意しておきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/cc547492-4c3f-58fe-f7ac-61ca70c029d6.png)

```html:nodejs\public\udpsend_console\index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>

  <link rel="stylesheet" href="css/start.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spinkit/2.0.1/spinkit.min.css" />
  <script src="js/methods_bootstrap.js"></script>
  <script src="js/components_bootstrap.js"></script>
  <script src="js/components_utils.js"></script>
  <script src="js/vue_utils.js"></script>
  <script src="js/gql_utils.js"></script>

  <script src="js/remoteconsole.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vconsole/dist/vconsole.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuex@3.x/dist/vuex.min.js"></script>

  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>

  <title>UDP送信コンソール</title>
</head>
<body>
<!--
    <div id="loader-background">
      <div class="sk-plane sk-center"></div>
    </div>
-->
    <div id="top" class="container">
      <div class="jumbotron">
        <h2>UDP送信コンソール</h2>
      </div>

      <div class="form-inline">
        <label>IPアドレス</label> <input type="text" class="form-control" v-model="ipaddress">
        <label>ポート番号</label> <input type="number" class="form-control" v-model.number="port">
      </div>

      <br>

      <div class="panel panel-primary">
        <div class="panel-heading"><h2 class="panel-title">toast</h2></div>
        <div class="panel-body">
          <label>message</label> <input type="text" class="form-control" v-model="param_toast.message">
        </div>
        <div class="panel-footer text-right">
          <button class="btn btn-default" v-on:click="send_toast">送信</button>
        </div>
      </div>

      <div class="panel panel-primary">
        <div class="panel-heading"><h2 class="panel-title">notification</h2></div>
        <div class="panel-body">
          <label>title</label> <input type="text" class="form-control" v-model="param_notification.title">
          <label>message</label> <input type="text" class="form-control" v-model="param_notification.message">
        </div>
        <div class="panel-footer text-right">
          <button class="btn btn-default" v-on:click="send_notification">送信</button>
        </div>
      </div>

      <div class="panel panel-primary">
        <div class="panel-heading"><h2 class="panel-title">free</h2></div>
        <div class="panel-body">
          <textarea class="form-control" rows="5" v-model="param_free"></textarea>
        </div>
        <div class="panel-footer text-right">
          <button class="btn btn-default" v-on:click="send_free">送信</button>
        </div>
      </div>


      <!-- for progress-dialog -->
        <progress-dialog v-bind:title="progress_title"></progress-dialog>
    </div>

    <script src="js/store.js"></script>
    <script src="js/start.js"></script>
</body>
```

以上