@blog.justoneplanet.info

日々勉強

AndroidでWebSocket接続する

素晴らしいライブラリのお陰で5分で接続できる。

■Activityで使う

package info.justoneplanet.android.eew;
import java.net.URI;
import java.net.URISyntaxException;
import org.java_websocket.WebSocketClient;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.handshake.ServerHandshake;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class EarthquakeEarlyWarningActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try {
            URI uri = new URI("ws", null, "123.123.123.123", 443, "/", null, null);
            WebSocketClient c = new WebSocketClient(uri, new Draft_17()) {
                @Override
                public void onOpen(ServerHandshake handshakedata) {
                    Log.e("onOpen", handshakedata.toString());
                }
                @Override
                public void onMessage(String message) {
                    Log.e("onMessage", message);
                }
                @Override
                public void onError(Exception ex) {
                    Log.e("onError", ex.toString());
                }
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    Log.e("onClose", reason.toString());
                }
            };
            c.connect();
        }
        catch (URISyntaxException e) {
            e.printStackTrace();
        }
   }
}

あとは以下のパーミッションを付加する。

<uses-permission android:name="android.permission.INTERNET" />

■AppWidgetなどで使う

メインスレッド以外で使うには以下のようにする。

package info.justoneplanet.android.eew;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;

import org.java_websocket.WebSocketClient;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Notification;
import android.app.NotificationManager;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;

public class Widget extends AppWidgetProvider {
    private WebSocketClient client;
    private HandlerThread sWorkerThread;
    private Handler sWorkerQueue;
    private Server[] mServers;

    private class Server {
        String host;
        int port;
        public Server(String host, int port) {
            this.host = host;
            this.port = port;
        }
    }

    public Widget() {
        super();
        sWorkerThread = new HandlerThread("Widget-worker");
        sWorkerThread.start();
        sWorkerQueue = new Handler(sWorkerThread.getLooper());
        mServers = new Server[]{
                new Server("123.123.123.123", 80)
        };
    }

    @Override
    public void onEnabled(final Context context) {
        super.onEnabled(context);
    }

    @Override
    public void onUpdate(final Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        Log.e("onUpdate", "onUpdate");
        sWorkerQueue.removeMessages(0);
        try {
            Random rand = new Random();
            Server server = mServers[rand.nextInt(mServers.length) % mServers.length];
            URI uri = new URI("ws", null, server.host, server.port, "/", null, null);
            sWorkerQueue.post(new WebSocketClient(uri, new Draft_17()) {
                private String chunk;
                private RemoteViews remoteViews;

                @Override
                public void onOpen(ServerHandshake handshakedata) {
                    Log.e("onOpen", handshakedata.toString());
                    Toast.makeText(context, handshakedata.toString(), Toast.LENGTH_LONG);
                    remoteViews = new RemoteViews(context.getPackageName(), R.layout.main);
                }

                @Override
                public void onMessage(String message) {
                    Log.e("onMessage", message);
                    remoteViews.setTextViewText(R.id.info, context.getString(R.string.connected));
                    ComponentName componentName = new ComponentName(context, Widget.class);
                    AppWidgetManager manager = AppWidgetManager.getInstance(context);
                    manager.updateAppWidget(componentName, remoteViews);
                }

                @Override
                public void onError(Exception ex) {
                    Log.e("onError", ex.toString());
                    remoteViews.setTextViewText(R.id.info, ex.toString());
                }

                @Override
                public void onClose(int code, String reason, boolean remote) {
                    Log.e("onClose", reason);
                    remoteViews.setTextViewText(R.id.info, reason);
                    client.connect();
                }
            });
        }
        catch (URISyntaxException e1) {
            e1.printStackTrace();
        }
    }
}

再接続処理とかしないといけない。

CakePHP2系で認証のユーザーモデルを変更する

ログアウト後のログインが上手くできなくてハマったのでメモしておく。

class AppController extends Controller {
    public $components = array(
        'Auth' => array(
            'loginRedirect' => array('controller' => 'hoge', 'action' => 'index'),
            'loginAction'   => array('controller' => 'hoge', 'action' => 'login'),
            'authenticate'  => array(
                'Form' => array(
                    'userModel' => 'Hoge',
                    'fields' => array(
                        'username' => 'email',
                    ),
                    'scope' => array(
                        'Hoge.is_public' => 1,
                    ),
                ),
            ),
        ),
        'Session'
    );

    public function beforeFilter() {
        parent::beforeFilter();
    }
}
class HogeController extends AppController {
    public function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow(array('login', 'logout', 'fuga'));// loginとlogoutも記述しておく
    }
    public function login() {
        if ($this->Auth->login()) {
            $this->redirect($this->Auth->redirect());
        }
        else {
            $this->Session->setFlash(__('Invalid username or password, try again'));
        }
    }
    public function logout() {
        $this->redirect($this->Auth->logout());
    }
}

UIWebViewをカスタマイズする

WebViewっぽくならないようにバウンスをさせないようにし、HTTPリクエストにカスタムヘッダーを付加するようにした。たいしてカスタマイズしてない。

#import <UIKit/UIKit.h>
@interface CustomWebView : UIWebView
@end
#import "CustomWebView.h"
@implementation CustomWebView
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {// Initialization code
        // バウンスさせない
        for (id subview in self.subviews) {
            if ([[subview class] isSubclassOfClass: [UIScrollView class]]) {
                ((UIScrollView *)subview).bounces = NO;
            }
        }
    }
    return self;
}

- (void)loadRequest:(NSURLRequest *)request
{
    // カスタムヘッダーの付加
    NSMutableURLRequest *mutableRequest = (NSMutableURLRequest *)[request mutableCopy];
    [mutableRequest setValue:@"iOS.UIWebView" forHTTPHeaderField:@"App-Client"];
    [super loadRequest:mutableRequest];
}
@end

以下のようにオフライン時の画面を表示するメソッドを作ってあげてもいいと思う。

- (void)displayOfflineView
{
    if (self) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"offline" ofType:@"html"];
        [self loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
    }
}

webviewを表示した時に圏外の場合、圏内になった時にreloadをしても正しく表示ができないので以下のようなメソッドも加えておく。

- (void)dealloc
{
    if (initialRequest) {
        [initialRequest release];
    }
    [super dealloc];
}
- (void)reload
{
    if (self) {
        if ([self canGoBack]) {
            [super reload];
        }
        else if (initialRequest) {
            [self loadRequest:initialRequest];
        }
    }
}
- (void)loadRequest:(NSURLRequest *)request
{
    [super loadRequest:request];
    if (!initialRequest) {
        initialRequest = [request retain];
    }
}

NSURLのURLの各部分に対するアクセス方法を調べてみる

分かりやすいものから分かりにくいものまであるのでとりあえずメモしておく。

// http://user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
NSURL *url = [request URL];
[url scheme];// http
[url host];// hogehoge.com
[url absoluteString];// http://user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
[url absoluteURL];// http://user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
[url baseURL];
[url fragment];// hoge
[url host];// hogehoge.com
[url lastPathComponent];// test.html
[url parameterString];
[url password];// pass
[url path];// /dir/use/test.html
[url pathComponents];// ("/",dir,d,"test.html")
[url pathExtension];// html
[url port];// 1234
[url query];// q=sample
[url relativePath];// /dir/use/test.html
[url relativeString];// http://user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
[url resourceSpecifier];// //user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
[url standardizedURL];// http://user:pass@hogehoge.com:1234/dir/d/test.html?q=sample#hoge
[url user];// user

HMAC-MD5を計算する

■PHP

hash_hmac('md5', $str, 'hogehogehoge');

■Android

public class HmacMD5 {
    private static final String ALGORISM = "HmacMD5";
    private static final String S = "hogehogehoge";
    public static String get(String str) {
        SecretKeySpec secretKeySpec = new SecretKeySpec(S.getBytes(), ALGORISM);
        try {
            Mac mac = Mac.getInstance(ALGORISM);
            mac.init(secretKeySpec);
            byte[] result = mac.doFinal(str.getBytes());
            return byteToString(result);
        }
        catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        catch (InvalidKeyException e) {
            e.printStackTrace();
        }
        return "";
    }

    private static String byteToString(byte [] b) {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < b.length; i++) {
            int d = b[i];
            d += (d < 0)? 256 : 0;
            if (d < 16) {
                buffer.append("0");
            }
            buffer.append(Integer.toString(d, 16));
        }
        return buffer.toString();
    }
}

■iOS

日本語入力に対応するためにstackoverflowから持ってきたコードに少々手を入れた。

+ (NSString *)HMACMD5WithKey:(NSString *)data
{
    const char *cKey  = [@"hogehogehoge" cStringUsingEncoding:NSUTF8StringEncoding];
    const char *cData = [data cStringUsingEncoding:NSUTF8StringEncoding];
    const unsigned int blockSize = 64;
    char ipad[blockSize];
    char opad[blockSize];
    char keypad[blockSize];

    unsigned int keyLen = strlen(cKey);
    CC_MD5_CTX ctxt;
    if (keyLen > blockSize) {
        CC_MD5_Init(&ctxt);
        CC_MD5_Update(&ctxt, cKey, keyLen);
        CC_MD5_Final((unsigned char *)keypad, &ctxt);
        keyLen = CC_MD5_DIGEST_LENGTH;
    }
    else {
        memcpy(keypad, cKey, keyLen);
    }

    memset(ipad, 0x36, blockSize);
    memset(opad, 0x5c, blockSize);

    int i;
    for (i = 0; i < keyLen; i++) {
        ipad[i] ^= keypad[i];
        opad[i] ^= keypad[i];
    }

    CC_MD5_Init(&ctxt);
    CC_MD5_Update(&ctxt, ipad, blockSize);
    CC_MD5_Update(&ctxt, cData, strlen(cData));
    unsigned char md5[CC_MD5_DIGEST_LENGTH];
    CC_MD5_Final(md5, &ctxt);

    CC_MD5_Init(&ctxt);
    CC_MD5_Update(&ctxt, opad, blockSize);
    CC_MD5_Update(&ctxt, md5, CC_MD5_DIGEST_LENGTH);
    CC_MD5_Final(md5, &ctxt);

    const unsigned int hex_len = CC_MD5_DIGEST_LENGTH*2+2;
    char hex[hex_len];
    for(i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        snprintf(&hex[i*2], hex_len-i*2, "%02x", md5[i]);
    }

    NSData *HMAC = [[NSData alloc] initWithBytes:hex length:strlen(hex)];
    NSString *hash = [[[NSString alloc] initWithData:HMAC encoding:NSUTF8StringEncoding] autorelease];
    [HMAC release];
    return hash;
}

ZXingをXcodeプロジェクトに組み込む

■組込

zxing-x.x/iphone/ZXingWidget/を自身のプロジェクトのプロジェクトナビゲーターにドロップする。

Targets > Build Phases

Target DependenciesでZXingWidgetを追加した後、Link Binary With Librariesで以下を追加する。

  • libZXingWidget.a
  • libiconv.dylib
  • CoreMedia.framework
  • CoreVideo.framework
  • AVFoundation.framework
  • AudioToolbox.framework
  • AddressBook.framework
  • AddressBookUI.framework
  • CoreGraphics.framework

Project > Build Settings

Header Search Pathsに以下の項目を追加する。

  • /User/[your name]/zxing-x.x/iphone/ZXingWidget/Classes
  • /User/[your name]/zxing-x.x/cpp/core/src

前者はrecursiveにチェックを入れて、後者はチェックを入れない。

参考

■実装

ScanViewController.h

#import <UIKit/UIKit.h>
#import "ZXingWidgetController.h"

@interface ScanViewController : UIViewController<ZXingDelegate>{
    UITextView *resultView;
    NSString *resultStr;
}
@property (nonatomic, retain) UITextView *resultView;
@property (nonatomic, copy) NSString *resultStr;
- (void)scanPressed:(id)sender;
@end

ScanViewController.mm

ファイルの拡張子を.mmとしないとコンパイルできない。

#import "ScanViewController.h"
#import "QRCodeReader.h"

@implementation ScanViewController
@synthesize resultView;
@synthesize resultStr;

- (void)scanPressed:(id)sender
{
    ZXingWidgetController *widController = [[ZXingWidgetController alloc] initWithDelegate:self showCancel:YES OneDMode:NO];
    QRCodeReader* qrcodeReader = [[QRCodeReader alloc] init];
    NSSet *readers = [[NSSet alloc ] initWithObjects:qrcodeReader,nil];
    [qrcodeReader release];
    widController.readers = readers;
    [readers release];

    [self presentModalViewController:widController animated:YES];
    [widController release];
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
    }
    return self;
}
- (void)dealloc {
    [resultView release];
}

#pragma mark - View lifecycle
- (void)loadView
{
    [super loadView];

    UIButton *btnStamp = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [btnStamp setFrame:CGRectMake(40, 220, 240, 50)];
    [btnStamp setTitle:@"QRコードを読み取る" forState:UIControlStateNormal];
    [btnStamp setTag:1];
    [btnStamp addTarget:self action:@selector(scanPressed:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btnStamp];

    resultView = [[UITextView alloc] initWithFrame:CGRectMake(40 , 280, 240, 50)];
    [self.view addSubview:resultView];
}
- (void)viewDidUnload
{
    [super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark -
#pragma mark ZXingDelegateMethods
- (void)zxingController:(ZXingWidgetController*)controller didScanResult:(NSString *)result {
    self.resultStr = result;
    if (self.isViewLoaded) {
        [resultView setText:resultStr];
        [resultView setNeedsDisplay];
    }
    [self dismissModalViewControllerAnimated:NO];
}
- (void)zxingControllerDidCancel:(ZXingWidgetController*)controller {
    [self dismissModalViewControllerAnimated:YES];
}
@end

こんな感じで。

AccessoryViewをカスタマイズする

setAccessoryViewしただけだとタップした時にaccessoryButtonTappedForRowWithIndexPathが発火しなくなるので以下のようにする。

// create cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // call cell
    UITableViewCellFixed *cell = (UITableViewCellFixed *)[tableView dequeueReusableCellWithIdentifier:@"table_cell"];
    if (cell == nil) {// create cell
        cell = [[[UITableViewCellFixed alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"table_cell"] autorelease];
    }

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setFrame:CGRectMake(0, 0, 26, 26)];
    [button setBackgroundImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor clearColor]];
    [button addTarget:self action:@selector(accessoryButtonTapped:withEvent:) forControlEvents:UIControlEventTouchUpInside];
    [cell setAccessoryView:button];
    return cell;
}
- (void)accessoryButtonTapped:(UIControl *)button withEvent:(UIEvent *)event
{
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:[[[event touchesForView: button] anyObject] locationInView:self.tableView]];
    if (indexPath == nil) {
        return;
    }
    [self.tableView.delegate tableView: self.tableView accessoryButtonTappedForRowWithIndexPath:indexPath];
}

EC2にexpressを入れる

■インストール

yum install openssl-devel gcc-c++ make#前準備
wget http://nodejs.org/dist/v0.6.11/node-v0.6.11.tar.gz
tar xvzf node-v0.6.11.tar.gz
cd node-v0.6.11
./configure
make
make install
curl http://npmjs.org/install.sh | sh
npm install -g express
express sample
cd sample
npm install

./app.js

たまたま80番開いていたので以下のようにして80番で繋がるようにする。

/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes');

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes

app.get('/', routes.index);

app.listen(80);
console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);

■起動

node app.js

http://ec2-123-123-123-123.ap-northeast-1.compute.amazonaws.com/などで自分のインスタンスにアクセスすれば見れる。

■socket.ioを使う

./package.json

以下のように編集する。

{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.8"
    , "jade": ">= 0.0.1"
    , "socket.io": "*"
  }
}

./sampleで以下のコマンドを実行する。

npm install

./app.js

サーバー側のコードをapp.jsの最後尾に付加する。

var io = socketio.listen(app)
   ,count = 0;
io.sockets.on('connection', function(socket){
  count++;
  io.sockets.emit('count change', count);
  socket.on('disconnect', function() {
    count--;
    socket.broadcast.emit('count change', count);
  }
  );
});

./views/layout.jade

クライアント側の処理を書き加える。

!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://code.jquery.com/jquery.min.js')
    script(type='text/javascript', src='/socket.io/socket.io.js')
    script(type='text/javascript')
      var socket = io.connect();
      socket.on('count change', function(count) {
        $('#count').text(count);
      });
  body!= body

./views/index.jade

表示用に要素を追加する。

#page
  #headerArea
    h1= title
    p Welcome to #{title}
    p 現在このページを見ている人は
    span#count
    人います。

■ejsを使う

既存のhtmlコードを使用するときにhamlにするのは手間なのでejsを使う。

./package.json

以下のように編集する。

{
    "name": "application-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.8"
    , "jade": ">= 0.0.1"
    , "socket.io": "*"
    , "ejs": "*"
  }
}

./app.js

以下のようにして、テンプレートエンジンをjadeからejsに変更する。

/**
 * Module dependencies.
 */

var express  = require('express')
  , socketio = require('socket.io')
  , ejs      = require('ejs')
  , routes   = require('./routes');

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options', {layout : false});
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

せっかくなんでルーティングも変更してみる。

app.get('/', routes.index);
app.get('/hoge', function(req, res) {
  res.render('hoge.ejs', { title: 'My Site' });
});

参考

./views/hoge.ejs

あとは以下のようにテンプレートファイルを用意する。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<title><%= title %></title>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
var socket = io.connect();
socket.on('count change', function(count) {
    $('#count').text(count);
});
</script>
</head>

<body>
<h1><%= title %></h1>
<p>現在このページを見ている人は<span id="count"></span>人います。</p>
</body>
</html>

以下のようなURLでアクセスができるようになる。


http://ec2-123-123-123-123.ap-northeast-1.compute.amazonaws.com/hoge

fragmentに値を渡す

■渡す側

FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();

SearchFragment fragment = (SearchFragment) Fragment.instantiate(getActivity(), SearchFragment.class.getName(), null);
Bundle bundle = new Bundle();
bundle.putString("word", word);
fragment.setArguments(bundle);

ft.replace(R.id.content, fragment).addToBackStack("result");
ft.commit();
getActivity().getSupportFragmentManager().executePendingTransactions();

■受け取り側

mWord = getArguments().getString("word");