temporaryなめも帳

だらだらと備忘録とか。誰かの為になることをねがって。

PUSHっぽい何かを実現する

あくまでもローカルな環境向け。基本的にはGCMをつかうべきです。 WebSocket覚えたし、使ってみたかってん。ぐらいのなにか。

Android側にPushを受けるサービスを実装する

アプリケーション内にServiceを実装し、ws://IPADDRESS:PORT/path/to/url に対しWebSocketを張る部分を作る。 WebSocketの実装には、 Java-WebSocket というライブラリを利用しました。

まず、build.gradleにライブラリをインポートする部分を書きます。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'org.java-websocket:Java-WebSocket:1.3.0@jar'
}

続いて、Activityの実装。 Activityには2個ボタンを用意して、ServiceのStop,Startの制御とServerへのメッセージを投げる部分をハンドリングしています。

    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        intent.setClass(getApplicationContext(), PushService.class);
        switch (view.getId()){
            case R.id.button1: {
                if (!isServiceRunning("com.kobashin.sample.PushService")) {
                    isRunning = true;
                    PushService.startForegroundService(getApplicationContext(), SERVER_URI);
                } else {
                    isRunning = false;
                    PushService.stopForegroundService(getApplicationContext());
                }
                break;
            }
            case R.id.button2: {
                PushService.sendMessageToServer(getApplicationContext(), "Hello Server");
            }
        }
    }

    private boolean isServiceRunning(String className){
        boolean ret = false;

        ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(Integer.MAX_VALUE);

        for(ActivityManager.RunningServiceInfo info : services){
            if(info.service.getClassName().equals(className)){
                ret = true;
            }
        }
        return ret;
    }

isServiceRunning()メソッドはServiceの実行状態の確認用のメソッドです。 Serviceへの通知にはService側にpublic staticで実装したメソッド(PushService.startForegroundServiceなど)を利用しています。

さらに、Service側の実装。ぺたっと全部。

public class PushService extends Service {

    private static String LOG_TAG = "PushService";

    private NotificationManager mNotificationManager;

    private final IBinder mBinder = new LocalBinder();

    private static final String ACTION_START_FOREGROUND
            = "com.kobashin.sample.START_FOREGROUND";

    private static final String ACTION_STOP_FOREGROUND
            = "com.kobashin.sample.STOP_FOREGROUND";

    private static final String ACTION_SEND_MESSAGE_TO_SERVER
            = "com.kobashin.sample.SEND_MESSAGE_TO_SERVER";

    private WebSocketClient mWebSocketClient;

    private HandlerThread mHandlerThread;

    private MsgHandler mMsgHandler;


    private enum NOTIFY_ID {
        NOTIFICATION_STARTED(100),
        NOTIFICATION_END(101),
        NOTIFICATION_RECEIVE_MESSAGE(102);

        NOTIFY_ID(int i) {
            id = i;
        }

        private int id;

        public int getId() {
            return id;
        }
    }


    private class MsgHandler extends Handler {

        public MsgHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 0: //init
                    handleInitAction(msg);
                    break;
                case 1: //onMessage
                    handleMessageAction(msg);
                    break;
                case 2: //send message
                    handleSendMessageAction(msg);
                default:
                    break;
            }
        }

        private void handleSendMessageAction(Message msg) {
            try {
                mWebSocketClient.send((String) msg.obj);
            } catch (NotYetConnectedException e) {
                e.printStackTrace();
            }
        }

        private void handleMessageAction(Message msg) {
            showNotification(NOTIFY_ID.NOTIFICATION_RECEIVE_MESSAGE.getId(), "GetMessage",
                    (String) msg.obj, false);
        }

        private void handleInitAction(Message msg) {
            try {
                mWebSocketClient = new WebSocketClient(new URI((String) msg.obj)) {
                    @Override
                    public void onOpen(ServerHandshake handshakedata) {
                        Log.i(LOG_TAG, "[WebSocketClient] onOpen");
                    }

                    @Override
                    public void onMessage(String message) {
                        Log.i(LOG_TAG, "[WebSocketClient] onMessage: " + message);
                        Message msg = mMsgHandler.obtainMessage();
                        msg.what = 1; // onMessage
                        msg.obj = message;
                        mMsgHandler.sendMessage(msg);
                    }

                    @Override
                    public void onClose(int code, String reason, boolean remote) {
                        Log.i(LOG_TAG,
                                "[WebSocketClient] onClose: code(" + code + ") reason(" + reason
                                        + ") remote(" + remote + ")");
                    }

                    @Override
                    public void onError(Exception ex) {
                        Log.i(LOG_TAG, "[WebSocketClient] onError", ex);
                    }
                };
                mWebSocketClient.connect();
            } catch (URISyntaxException e) {
                e.printStackTrace();

                // TODO: stop & notify
            }
        }
    }


    public static void startForegroundService(Context context, String serverUri) {
        Intent intent = new Intent(context, PushService.class);
        intent.putExtra("SERVER_URI", serverUri);
        intent.setAction(ACTION_START_FOREGROUND);
        context.startService(intent);
    }

    public static void stopForegroundService(Context context) {
        Intent intent = new Intent(context, PushService.class);
        intent.setAction(ACTION_STOP_FOREGROUND);
        context.startService(intent);
    }

    public static void sendMessageToServer(Context context, String message) {
        Intent intent = new Intent(context, PushService.class);
        intent.putExtra("SEND_MESSAGE", message);
        intent.setAction(ACTION_SEND_MESSAGE_TO_SERVER);
        context.startService(intent);
    }


    public PushService() {
    }


    public class LocalBinder extends Binder {

        PushService getService() {
            return PushService.this;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent.getAction().equals(ACTION_START_FOREGROUND)) {
            startForeground(NOTIFY_ID.NOTIFICATION_STARTED.getId(), createNotification(
                    NOTIFY_ID.NOTIFICATION_STARTED.getId(), true));

            mHandlerThread = new HandlerThread("push_service", Process.THREAD_PRIORITY_FOREGROUND);
            mHandlerThread.start();
            mMsgHandler = new MsgHandler(mHandlerThread.getLooper());

            Message msg = mMsgHandler.obtainMessage();
            msg.what = 0; //init
            msg.obj = intent.getStringExtra("SERVER_URI");
            mMsgHandler.sendMessage(msg); // init
        } else if (intent.getAction().equals(ACTION_STOP_FOREGROUND)) {
            stopForeground(true);
            stopSelf();
        } else if (intent.getAction().equals(ACTION_SEND_MESSAGE_TO_SERVER)) {
            Message msg = mMsgHandler.obtainMessage();
            msg.what = 2; //send message
            msg.obj = intent.getStringExtra("SEND_MESSAGE");
            mMsgHandler.sendMessage(msg);
        }
        return START_STICKY;
    }

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

    @Override
    public void onDestroy() {
        super.onDestroy();
        cancelNotification(NOTIFY_ID.NOTIFICATION_STARTED.getId());
        mWebSocketClient.close();
        mHandlerThread.quit();
    }

    private void showNotification(int id, String title, String msg, boolean isOnGoing) {
        PendingIntent pendingIntent = PendingIntent
                .getActivity(getApplicationContext(), 0, new Intent(), 0);

        Notification.Builder builder = new Notification.Builder(getApplicationContext())
                .setContentTitle(title)
                .setContentText(msg)
                .setSmallIcon(R.mipmap.ic_notificataion)
                .setColor(0x4fc3f7)
                .setPriority(Notification.PRIORITY_HIGH)
                .setFullScreenIntent(pendingIntent, true)
                .setOngoing(isOnGoing) // can't dismiss
                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
        mNotificationManager.notify(id, builder.build());
    }

    private Notification createNotification(int id, boolean isOnGoing) {
        PendingIntent pendingIntent = PendingIntent
                .getActivity(getApplicationContext(), 0, new Intent(), 0);

        Notification.Builder builder = new Notification.Builder(getApplicationContext())
                .setContentTitle("PushService")
                .setContentText("push service started")
                .setSmallIcon(R.mipmap.ic_notificataion)
                .setColor(0x4fc3f7)
                .setPriority(Notification.PRIORITY_HIGH)
                .setFullScreenIntent(pendingIntent, true)
                .setOngoing(isOnGoing) // can't dismiss
                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION));
        return builder.build();
    }

    private void cancelNotification(int id) {
        mNotificationManager.cancel(id);
    }

}

handleInitAction()内でWebsocketのinit処理を行っています。 WebSocketClientはライブラリに用意されたクラスで、作成時にURI(ここでは ws://192.168.11.4:5000/socket_handset/bing)を渡します。 Soceketを閉じるときはWebSocketClient.close()を呼び出します。 作成したSocketにメッセージを受けると、WebSocketClient.onMessage()を受けることができます。

Websocketからメッセージを受けると、Notificationを出すように実装してあるので、Socketにサーバーからメッセージを入れればPUSHっぽいことが実現できます。

Server側の実装

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask, request
from flask_sockets import Sockets
from werkzeug.exceptions import abort
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler
from geventwebsocket.websocket import WebSocketError

app = Flask(__name__)
sockets = Sockets(app)
ws_list = set()


@sockets.route('/socket_handset/bind')
def bind_sockets(ws):
    '''
    handsetとのwebsocket接続用
    :param ws:
    :return:
    '''
    if not ws:
        abort(400)

    ws_list.add(ws)
    remove = set()
    print 'adding', len(ws_list)

    while True:
        try:
            msg = ws.receive()
            print "get message: ", msg
            for s in ws_list:
                try:
                    s.send(msg)
                except Exception:
                    remove.add(s)
                    break

        except WebSocketError:
            remove.add(ws)
            break

    for s in remove:
        ws_list.remove(s)


@app.route('/send/message', methods=['POST'])
def send_message():
    '''
    JSONがPOSTされたら、つながっているWebsocketにデータを流す
    JSONは以下形式を期待している
    {
        "message": "message to send"
    }

    curlでのJSONのPOSTは以下でできる
    curl -vv -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"message": "message from server"}' http://localhost:5000/send/message
    :param message:
    :return:
    '''
    if request.method == 'POST':
        print "send_message", request.json

        json_data = request.json
        print len(ws_list)

        remove = set()
        for s in ws_list:
            try:
                s.send(json_data["message"])
            except Exception:
                remove.add(s)

        for s in remove:
            ws_list.remove(s)

        return 'ok'
    abort(400)



@app.route('/')
def hello_world():
    return 'Hello World!'


if __name__ == '__main__':
    server = pywsgi.WSGIServer(('', 5000), app, handler_class=WebSocketHandler)
    server.serve_forever()

PUSH確認は以下curlで。

$ curl -vv -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"message": "message from server"}' http://localhost:5000/send/message

Notificationが出たらOK!