@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];
    }
}

■カスタムヘッダの付加

ただし、上述の方法ではUIWebView内のリンクをクリックした時にはカスタムヘッダが付加されないので以下のようにする。

- (void) _internalInit {
    super.delegate = self;
    foreignDelegate = nil;
}
- (id) init {
    if((self = [super init])) {
        [self _internalInit];
    }
    return self;
}
- (id) initWithCoder:(NSCoder *)aDecoder {
    if((self = [super initWithCoder:aDecoder])) {
        [self _internalInit];
    }
    return self;
}
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {// Initialization code
        [self _internalInit];
    }
    return self;
}
- (void)dealloc
{
    [super dealloc];
}
// offline画面を表示する
- (void)displayOfflineView
{
    if (self) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"offline" ofType:@"html"];
        [self loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
    }
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/
- (void) setDelegate:(id<UIWebViewDelegate>)aDelegate {
    if(foreignDelegate == aDelegate) {
        return;
    }
	
    [foreignDelegate release];
    foreignDelegate = [aDelegate retain];
    /* Make sure this class is still the real delegate */
    super.delegate = self;
}
- (id<UIWebViewDelegate>) delegate {
    return foreignDelegate;
}
- (void) setCustomHeaders:(NSDictionary *)cHeaders {
    NSMutableDictionary *newHeaders = [NSMutableDictionary dictionary];
    for(NSString *key in [cHeaders allKeys]) {
        NSString *lowercaseKey = [key lowercaseString];
        [newHeaders setObject:[cHeaders objectForKey:key] forKey:lowercaseKey];
    }
    [customHeaders release];
    customHeaders = [newHeaders retain];
}
#pragma mark -
#pragma mark UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)aRequest navigationType:(UIWebViewNavigationType)navigationType {    
    
    BOOL missingHeaders = NO;
    
    /* Take all headers keys, lowercase them */
    NSArray *currentHeaders = [[aRequest allHTTPHeaderFields] allKeys];
    NSMutableArray *lowercasedHeaders = [NSMutableArray array];
    for(NSString *key in currentHeaders) {
        [lowercasedHeaders addObject:[key lowercaseString]];
    }
    
    /* See if there's at least one custom header missing */
    for(NSString *key in customHeaders) {
        if(![lowercasedHeaders containsObject:key]) {
            missingHeaders = YES;
            break;
        }
    }
    
    /* If one is missing, drop current request, and make a new one
     * with custom header */
    if(missingHeaders) {
        NSMutableURLRequest *newRequest = [aRequest mutableCopy];
        for(NSString *key in [customHeaders allKeys]) {
            [newRequest setValue:[customHeaders valueForKey:key] forHTTPHeaderField:key];
        }
        [self loadRequest:newRequest];
        [newRequest release];
        return YES;
    }
    
    //NSLog(@"Loading request with HTTP headers %@", [aRequest allHTTPHeaderFields]);
    
    if([foreignDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
        return [foreignDelegate webView:self shouldStartLoadWithRequest:aRequest navigationType:navigationType];
    }
    
    return YES;
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
    if([foreignDelegate respondsToSelector:@selector(webViewDidStartLoad:)])
        [foreignDelegate webViewDidStartLoad:self];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if([foreignDelegate respondsToSelector:@selector(webViewDidFinishLoad:)])
        [foreignDelegate webViewDidFinishLoad:self];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    if([foreignDelegate respondsToSelector:@selector(webView:didFailLoadWithError:)])
        [foreignDelegate webView:self didFailLoadWithError:error];
}

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

面倒になってきた。