Androidスマホにおいて、常にUDPパケット受信を待ち受けるバックグラウンドサービスを作成します。
UDPパケットをAndroidのフォアグラウンドサービスとして常に待機状態とし、受信したUDPパケットの内容に応じて、処理をして、また待ち受けます。
できる処理は多くはないですが、今回は、トースト表示とNotification通知をします。
ソースコードもろもろをGitHubに上げておきました。
poruruba/UdpBackgroundService
フォアグラウンドサービスの立ち上げ
フォアグラウンドサービスの立ち上げには、MainActivityから以下を実行します。
Intent intent;
intent = new Intent(this, UdpBackgroundService.class);
intent.putExtra("udpPort", 1234);
startForegroundService(intent);
待ち受けるポート番号を渡してあげるようにしました。
詳細は以下をご参照ください。
M5StickCのボタン押下でスマホから現在地をしゃべらせる(1/2)
停止は以下です。
Intent intent = new Intent(this, UdpBackgroundService.class);
stopService(intent);
フォアグラウンドサービスの実装
Serviceを派生させたクラスとして実装します。以下のメソッドが大事です。
・onCreate
このクラスが生成されたときに呼ばれます。最初の共通処理として使います。
・onStartCommand
フォアグラウンドサービスとして開始するときに呼び出されます。
・onDestroy
フォアグラウンドサービスの停止を要求されたときに呼び出されます。
onCreate
Notificationのチャネルを設定します。フォアグラウンドサービスとして立ち上げるときには必須です。2つのチャネルを設定しますが、1つ目が必須の方です。後者は必須ではないですが、UDPパケットを受信したときにする処理として使います。
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の準備をしてからノーティフィケーションをしています。
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を派生させたスレッドクラスを作成しました。
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にして受信ループを抜け出させます。
force_stop = true;
if( udpReceive != null ) {
udpReceive.close();
udpReceive = null;
}
AndroidManifest.xml
以下のサービスの設定をapplicationのところに追記します。
<service
android:name=".UdpBackgroundService"
android:parentActivityName=".MainActivity"
android:enabled="true"
android:exported="true"></service>
また、必要なパーミッションがありますので、それも追記しておきます。それぞれ、フォアグラウンドサービスのためと、WiFiのIPアドレスの取得のための、UDPパケット受信のためです。
<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ページも用意しておきました。
<!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>
以上