2017年9月14日木曜日

Spring BootとSpring Securityでログインを実装する方法

Spring Bootな構成でSpring Securityを用いて一般的なログイン機構を実装する方法をまとめる。ここでは、フロントをjavascriptで、サーバー側はJSONを返すAPIとして実装するような構成を想定する。また、認証情報はRDBMS上のユーザー情報テーブルにて管理する。

Spring Securityの機能全般を有効にする

まずは、WebSecurityConfigurerAdapterを継承したクラスを作成し、@EnableWebSecurityアノテーションを付与してSpring Securityの機能を有効化する。

    
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    // 略
}

Spring Securityの設定

これらの設定は、WebSecurityConfigurerAdapterのconfigure(HttpSecurity)メソッドをオーバーライドして実装する。ここでのポイントは4つ。

  • CSRF対策を無効化(今回は無関係なので無効化しているけど、別途CSRF対策は設定すべし)
  • ログイン認証を行うパスを設定
  • フォーム認証を有効化
  • POST /loginでログイン処理がトリガーされる(カスタマイズすることも可能)

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * {@inheritDoc}
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //
        // CSRF対策を無効化
        //
        http.csrf().disable();

        //
        // ログイン認証を行うパスを設定
        //
        http.authorizeRequests()
            // ログイン無しでアクセス許可するパス
            .antMatchers("/").permitAll()
            // その他はログインが必要
            .anyRequest().authenticated();

        //
        // フォーム認証を有効化
        //
        http.formLogin()
    }
}

ログインが成功/失敗した場合の処理を実装

ログインが成功/失敗した場合に、それぞれJSON形式のレスポンスを返すように実装する。

まずは、ログイン成功のハンドラー(AuthenticationSuccessHandler)とログイン失敗のハンドラー(AuthenticationFailureHandler)を定義する。


/**
 * ログイン成功時の動作を定義
 */
private static final AuthenticationSuccessHandler LOGIN_SUCCESS = (req, res, auth) -> {
    // HTTP Statusは200
    res.setStatus(HttpServletResponse.SC_OK);

    // Content-Type: application/json
    res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

    // Body
    res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code", "login_success")));
    res.getWriter().flush();
};

/**
 * ログイン失敗時の動作を定義
 */
private static final AuthenticationFailureHandler LOGIN_FAILED = (req, res, auth) -> {
    // HTTP Statusは401
    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    // Content-Type: application/json
    res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

    // Body
    res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code", "login_failed")));
    res.getWriter().flush();
};

ここで使っているImmutableMapは、Guavaライブラリのクラス。JsonUtilはObjectMapperを利用するための自作ユーティリティクラスとする。ObjectMapperは、別途DIコンテナに登録しておいたものを使うのが良さそうだが、ここでは主題ではないため適当に下記のように実装しておく。

public class JsonUtil {
    public static String encode(Object src) {
        try {
            return new ObjectMapper().writeValueAsString(src);
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

最後に、前項で設定したフォーム認証の有効化の箇所へ、ハンドラーの紐付け設定を追記する。

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * {@inheritDoc}
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 中略

        //
        // フォーム認証を有効化
        //
        http.formLogin()
            //
            // ログイン成功のハンドラーを設定
            //
            .successHandler(LOGIN_SUCCESS)
            //
            // ログイン失敗のハンドラーを設定
            //
            .failureHandler(LOGIN_FAILED);
    }
}

未ログインアクセスの制御

このままだと、ログインが必要なURL(例えば/test)にアクセスすると、/loginへのリダイレクトがレスポンスされる。Ajaxな通信を行う前提なので、ログインが必要な旨を示すJSONを返すように実装する。

まずは、認証エントリーポイントのハンドラー(AuthenticationEntryPoint)を定義する。

/**
 * 認証エントリポイントの動作を定義
 */
private static final AuthenticationEntryPoint LOGIN_REQUIRED = (req, res, auth) -> {
    // HTTP Statusは401
    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    
    // Content-Type: application/json
    res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);

    // Body
    res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code", "login_required")));
    res.getWriter().flush();
};

次に、前項と同様にconfigure()メソッドの中で、ハンドラーの紐付け設定を追記する。

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * {@inheritDoc}
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 中略

        http.exceptionHandling()
            //
            // 要ログインページアクセスのハンドラーを設定
            //
            .authenticationEntryPoint(LOGIN_REQUIRED);
    }
}

ユーザー情報を取得するサービスを定義

最後のステップとして、データベースからユーザー情報を取得して認証する部分を実装する。

まずは、データベースからユーザー情報を取得するサービスをUserDetailsServiceインターフェースの実装クラスとして定義する。

※実際には、JdbcTemplateではなくMyBatisやらDBFluteやらのお好きなO/Rマッパーを利用すると思う。

/**
 * ユーザー情報を取得するサービス
 */
@Service
public class MyUserDetailsService implements UserDetailsService {
    private final JdbcTemplate jdbcTemplate;

    private static final String SQL
        = "select password from user where name = ?";

    private static final SimpleGrantedAuthority ROLE
        = new SimpleGrantedAuthority("ROLE_USER");
    
    /**
     * Constructor
     */
    public MyUserDetailsService(final JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            throw new UsernameNotFoundException("No username");
        }

        // データベースから該当ユーザー情報を取得
        final String password = jdbcTemplate.queryForObject(
                SQL, new Object[]{username}, String.class);
        
        if (StringUtils.isEmpty(password)) {
            throw new UsernameNotFoundException("No user");
        }

        // ユーザー情報を生成
        return new User(
                username,
                password,
                Collections.singleton(ROLE));
    }
}

続いて、AuthenticationManagerBuilderへ、ユーザー情報取得サービスの紐付け設定を行う。

@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    private final MyUserDetailsService service;

    /**
     * Constructor
     */
    public MySecurityConfig(final MyUserDetailsService service) {
        this.service = service;
    }

    // 中略

    /**
     * {@inheritDoc}
     */
    @Autowired
    void configureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception {
        //
        // ユーザー情報取得サービスを紐付ける
        //
        auth.userDetailsService(service);
    }
}

GET /testの動作

要ログインURLへのアクセスは、ログインが必要な旨を示すJSONレスポンスが帰ってくる。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:24:14 GMT
Expires: 0
Pragma: no-cache
Set-Cookie: JSESSIONID=8A504C961F44779D571088EBFE5E7BA3; Path=/; HttpOnly
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_required"}

POST /loginの動作(ユーザー名やパスワードが正しくない)

所謂、ログインが失敗するパターン。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:29:08 GMT
Expires: 0
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_failed"}

POST /loginの動作(ユーザー名とパスワードが正しい)

ログインが成功するパターン。さらっと流したが、ログイン処理のデフォルト実装は、application/x-www-form-urlencodedなリクエストで、ユーザー名をusername、パスワードをpasswordという名前のパラメータとして送信する必要がある。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:35:07 GMT
Expires: 0
Pragma: no-cache
Set-Cookie: JSESSIONID=DA1076E9F2247B35BF5718CC75B9979B; Path=/; HttpOnly
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_success"}

2015年2月19日木曜日

Androidホームアプリ風のドラッグアンドドロップを実装する方法

目指すもの

グリッド表示されたアプリ一覧から、アイコン長押しでドラッグ&ドロップし、任意の場所へアプリを移動するUI

動画をとってみた


アプリの表示

まずは、アプリアイコンの表示を実装する。アプリアイコンは、TextViewへsetCompoundDrawablesWithIntrinsicBounds()メソッドでアイコン画像を指定することで実現する。

layout/appicon.xml

    
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingTop="4dp"
    android:textSize="12dp"
    android:singleLine="true"
    android:ellipsize="end"
    android:textColor="@android:color/white"
    android:gravity="center_horizontal|center_vertical" />

Javaコード

アプリアイコンを作るところ

// アイコン画像をロード(とりあえず電話っぽいアイコンにしとく)
Drawable icon = getResources().getDrawable(android.R.drawable.ic_menu_call);

// TextViewをXMLから生成
TextView appIcon
    = (TextView) getLayoutInflater().inflate(R.layout.appicon, null);
// アプリ名称を設定
appIcon.setText("でんわ");
// アイコンを設定(left, top, right, bottomの順で指定)
appIcon.setCompoundDrawables(null, icon, null, null);

アプリアイコンをコンテナ(ここではRelativeLayout)へ追加する

// コンテナへ追加
RelativeLayout c = (RelativeLayout) findViewById(R.id.container);
c.addView(appIcon);

Viewをドラッグ可能にする

次に、アプリアイコンをドラッグできるようにする。TextViewのstartDrag()メソッドを呼び出せば良く、長押しでドラッグ開始とするため、OnLongClickListenerを実装してアプリアイコンへセットする。

特筆すべき点として、ドロップ先へデータを渡すためstartDrag()の第3引数にViewそのものをセットしておく。

/**
 * Viewの長押しイベントをハンドリングするリスナークラス
 */
private static class MyLongClickListener implements View.OnLongClickListener
{
    /**
     * Viewが長押しされた際にシステムから呼び出される。
     */
    @Override
    public boolean onLongClick(View v)
    {
        // ドラッグ&ドロップで受け渡しするデータ(使わないのでダミー)
        ClipData tmpData = ClipData.newPlainText("dummy", "dummy");
        // ドラッグ中に表示するイメージのビルダー
        View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);

        // ドラッグを開始
        v.startDrag(tmpData, shadow, v, 0);
        
        return true;
    }
}

アプリアイコンへリスナーをセットする

MyLongClickListener listener = new MyLongClickListener();
appIcon.setOnLongClickListener(listener);

以上でドラッグできるようになっている、が、実行しても見た感じだと出来てるのか分からない。次項の「ドラッグ中に表示するイメージの制御」が必要。

ドラッグ中に表示するイメージの制御

基本的に、ドラッグ中に表示するイメージはDragShadowBuilderで制御しており、デフォルト実装(View#draw(Canvas)を呼び出す)で問題ない。

しかし、TextViewをドラッグ対象とした場合には意図した動作とならない。DragShadowBuilderクラスを継承して、TextViewのキャプチャを表示するようにカスタマイズする。

Javaコード

/**
 * ドラッグ&ドロップ中に表示するイメージを制御するクラス
 */
private static class MyDragShadowBuilder extends DragShadowBuilder
{
    /**
     * コンストラクタ
     */
    public MyDragShadowBuilder(View v) {
        super(v);
    }
    
    /**
     * ドラッグ中のイメージを描画する際にシステムが呼び出すメソッド
     */
    @Override
    public void onDrawShadow(Canvas canvas)
    {
        // ドラッグ対象View
        View view = getView();

        // Viewのキャプチャを取得する準備
        view.setDrawingCacheEnabled(true);
        view.destroyDrawingCache();

        // キャプチャを取得し、キャンバスへ描画する
        Bitmap bitmap = view.getDrawingCache();
        canvas.drawBitmap(bitmap, 0f, 0f, null);
    }
}

前述のOnLongClickListenerで実装していたイメージビルダーを作成したイメージビルダーへ置き換える。

...前略...
// これを
View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);
↓
// こうする
View.DragShadowBuilder shadow = new MyDragShadowBuilder(v);
...後略...

ドロップ先を用意する

最後に、ドロップ先としてViewGroup(LinearLayoutとか)を用意しておく。ViewGroupには、ドラッグイベントをハンドリングするためのOnDragListenerをセットすればOK。

OnDragListenerの実装は以下の通り

  • ACTION_DRAG_STARTEDを受けたらtrueを返す
  • ACTION_DROPを受けたらアプリアイコンを作ってViewGroupに追加する

Javaコード

/**
 * ドラッグイベントをハンドリングするリスナークラス
 */
private static class MyDragListener implements View.OnDragListener
{
    private LayoutInflater mInflater;

    /**
     * コンストラクタ
     */
    public MyDragListener(Context ctx)
    {
        mInflater = LayoutInflater.from(ctx);
    }

    /**
     * ドラッグイベントが発生した際に、システムから呼び出されるメソッド
     */
    @Override
    public boolean onDrag(View v, DragEvent event)
    {
        switch (event.getAction())
        {
            case DragEvent.ACTION_DRAG_STARTED:
            case DragEvent.ACTION_DRAG_ENTERED:
            case DragEvent.ACTION_DRAG_LOCATION:
            case DragEvent.ACTION_DRAG_EXITED:
                return true;

            case DragEvent.ACTION_DROP:
                //
                // ドラッグ元の情報を取得
                //
                // startDrag()の第3引数で渡したデータを取得
                TextView src = (TextView) event.getLocalState();
                // アプリ名
                CharSequence name = src.getText();
                // 画像
                Drawable[] imgs = src.getCompoundDrawables();

                //
                // 新しくドロップ先に設置するViewを生成
                //
                // アプリアイコンを新規作成(TextViewをXMLから生成)
                TextView appIcon
                    = (TextView) mInflater.inflate(R.layout.appicon, null);
                // アプリ名称を設定
                appIcon.setText(name);
                // 画像を設定
                appIcon.setCompoundDrawables(
                    imgs[0], imgs[1], imgs[2], imgs[3]);

                // ViewGroupへ追加
                ViewGroup c = (ViewGroup) v;
                c.addView(appIcon);

                return true;

            default:
                break;
        }

        return false;
    }
}

ドロップ先(ここではLinearLayout)へOnDragListenerをセットする

MyDragListener listener = new MyDragListener(this);
LinearLayout dropPlace = (LinearLayout) findViewById(R.id.drop_place);
dropPlace.setOnDragListener(mDragListener);

ドロップ先の背景

ドラッグしている際に、ドロップ先の背景が変わるようにしておく。以下のようにXMLで背景を作成し、ドロップ先に設定してあげるだけでOK

drawable/drop_place_background.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_drag_hovered="true">
        <shape>
            <solid android:color="#0affffff"></solid>
            <stroke
                android:color="#ffd700"
                android:dashgap="10dp"
                android:dashwidth="15dp"
                android:width="3dp">
        </stroke></shape>
    </item>
    
    <item>
        <shape>
            <solid android:color="#0affffff"></solid>
        </shape>
    </item>
    
</selector>

おまけ:アプリ一覧の取得

アプリ一覧を取得するコード例は以下の通り。

PackageManager manager = ctx.getPackageManager();

// アプリ一覧を取得する条件
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);

// アプリ一覧を取得
final List<ResolveInfo> apps = manager.queryIntentActivities(mainIntent, 0);
Collections.sort(apps, new ResolveInfo.DisplayNameComparator(manager));

// アプリ情報一覧を作成
for (ResolveInfo each : apps)
{
    // アプリ名を取得
    CharSequence label = each.loadLable(manager);
    // アイコンを取得
    Drawable icon = each.activityInfo.loadIcon(manager);
    
    ...略
}

2015年1月26日月曜日

Galaxy NexusにAndroid 5 Lollipopをインストールする

Android 5 Lollipopに対応したアプリケーションの開発やテストを実機で行うために、Galaxy NexusにカスタムROMを突っ込んでみた。

Android 5搭載端末と言えば、Google - Nexus 6かLG - G3かエミュレータか…どれも若干高いし、やっぱり実機が欲しい。

ちなみにGalaxy Nexusは¥15,000〜¥17,000くらいで手に入る!

やること


前提として、作業するPCにAndroid SDKをインストールしておくこと。
adbコマンドとfastbookコマンドを利用するので、$SDK_HOME/platform-toolsにパスを通しておくと便利。
  • ブートローダーのUnlock
  • TWRPの導入
  • ROOT化
  • 全パーティションのバックアップ
  • ROMのバックアップ
  • カスタムROMの導入

とりあえず


PCと端末をUSBケーブルで接続し、adbコマンドが実行出来る状態にしておく。

ブートローダーのUnlock


※この作業を行うと端末のデータが初期化されるため、必要に応じて先にバックアップを取得する必要がある。

まず、PCのターミナルで下記のコマンドを実行する。もしくは、電源ボタンとボリューム+/-ボタンの両方を押しっぱなしにして電源を入れる。
./adb reboot bootloader
上記の画面が表示されたら、ターミナルで下記のコマンドを実行し、Unlockする。
./fastboot oem unlock
確認画面が表示されるので、ボリューム+ボタンで選択し、電源ボタンで決定する。
うまくいけば下図のように、LOCK STATE - UNLOCKEDとなる。

TWRPの導入


TWRP(Team Win Recovery Project)からTWRP for Galaxy Nexus GSM [maguro]を辿って、imgファイルをダウンロードしておく。ここでは「openrecovery-twrp-2.8.4.0-maguro.img」を利用。

ブートローダーを起動した状態で、ターミナルから下記のコマンドでTWRPを起動する。
./fastboot boot ~/Downloads/openrecovery-twrp-2.8.4.0-maguro.img
起動画面はこんな感じ

ROOT化


TWRPのRebootメニューからSystemを選択すると自動的に端末が再起動して、SuperSuのインストールを促されるので指示に従えばOK。

全パーティションのバックアップ


念のため、全パーティションのバックアップ(userdata除く)を行っておく。まずは、ターミナルで下記コマンドを実行し、パーティション一覧を取得する。
./adb shell
cd /dev/block/platform/omap/omap_hsmmc.0/by-name
ls -l
こんな感じで表示される。
lrwxrwxrwx root     root              2015-01-23 03:43 boot -> /dev/block/mmcblk0p7
lrwxrwxrwx root     root              2015-01-23 03:43 cache -> /dev/block/mmcblk0p11
lrwxrwxrwx root     root              2015-01-23 03:43 dgs -> /dev/block/mmcblk0p6
lrwxrwxrwx root     root              2015-01-23 03:43 efs -> /dev/block/mmcblk0p3
lrwxrwxrwx root     root              2015-01-23 03:43 metadata -> /dev/block/mmcblk0p13
lrwxrwxrwx root     root              2015-01-23 03:43 misc -> /dev/block/mmcblk0p5
lrwxrwxrwx root     root              2015-01-23 03:43 param -> /dev/block/mmcblk0p4
lrwxrwxrwx root     root              2015-01-23 03:43 radio -> /dev/block/mmcblk0p9
lrwxrwxrwx root     root              2015-01-23 03:43 recovery -> /dev/block/mmcblk0p8
lrwxrwxrwx root     root              2015-01-23 03:43 sbl -> /dev/block/mmcblk0p2
lrwxrwxrwx root     root              2015-01-23 03:43 system -> /dev/block/mmcblk0p10
lrwxrwxrwx root     root              2015-01-23 03:43 userdata -> /dev/block/mmcblk0p12
lrwxrwxrwx root     root              2015-01-23 03:43 xloader -> /dev/block/mmcblk0p1
さらに、下記コマンドを1つずつ実行して、バックアップを行う。※mkdirで作成しているディレクトリ名を変更する場合や、上で取得したパーティション一覧に相違がある場合は、適宜コマンドを変更すること。
cd /sdcard
mkdir partbk20150123

dd if=/dev/block/mmcblk0p7 of=/sdcard/partbk20150123/mmcblk0p7_boot bs=4096
dd if=/dev/block/mmcblk0p11 of=/sdcard/partbk20150123/mmcblk0p11_cache bs=4096
dd if=/dev/block/mmcblk0p6 of=/sdcard/partbk20150123/mmcblk0p6_dgs bs=4096
dd if=/dev/block/mmcblk0p3 of=/sdcard/partbk20150123/mmcblk0p3_efs bs=4096
dd if=/dev/block/mmcblk0p13 of=/sdcard/partbk20150123/mmcblk0p13_metadata bs=4096
dd if=/dev/block/mmcblk0p5 of=/sdcard/partbk20150123/mmcblk0p5_misc bs=4096
dd if=/dev/block/mmcblk0p4 of=/sdcard/partbk20150123/mmcblk0p4_param bs=4096
dd if=/dev/block/mmcblk0p9 of=/sdcard/partbk20150123/mmcblk0p9_radio bs=4096
dd if=/dev/block/mmcblk0p8 of=/sdcard/partbk20150123/mmcblk0p8_recovery bs=4096
dd if=/dev/block/mmcblk0p2 of=/sdcard/partbk20150123/mmcblk0p2_sbl bs=4096
dd if=/dev/block/mmcblk0p10 of=/sdcard/partbk20150123/mmcblk0p10_system bs=4096
dd if=/dev/block/mmcblk0p1 of=/sdcard/partbk20150123/mmcblk0p1_xloader bs=4096
大体20分くらいはかかるので気長に作業を行う。

ROMのバックアップ


TWRPを起動してメニューのBakcupを選択する。対象項目(System、Data、Boot)をチェックして「Swipe to Back Up」をスワイプする。10分くらいでバックアップが完了し、Backup Completeが表示される。

画面に表示されたパス(/data/media/0/TWRP/BACKUPS/01498B2D17012007/2015-01-22--21-20-46 JDQ39.SC04DOMMD4/といった場所)にバックアップが作成される。

カスタムROMの導入


今回は、FML(Fork My Life)のAndroid Lollipop版を焼くので、下記2つのファイルをダウンロードしておく。

※ファイルについての詳細は、[ROM][5.0.2/LRX22G][AOSP][LINARO/OPTIMIZED] FML: Fork My Life (2015/01/08) - xdadevelopersを参照されたし

ダウンロードしたファイルは、ターミナルから下記のようにして端末へ転送しておく。
./adb shell
cd /sdcard/
mkdir tmp
exit

./adb push ~/Downloads/FML-AOSP-5.0-20150108-maguro.zip /sdcard/tmp/
./adb push ~/Downloads/pa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zip /sdcard/tmp/
次に、現在のROMを消す。TWRPを起動し、Wipe->Advanced Wipeを辿って以下を選択し「Swipe to Back Up」をスワイプする。
  • /data
  • /system
  • /cache
  • dalvik cache
最後に、TWRPのメニューからInstallを選択し、/sdcard/tmpのFML-AOSP-5.0-2015018-maguro.zipを選択する。

Installが完了したら、TWRPのメニューに戻ってInstallを選択し、/sdcard/tmpのpa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zipを選択する。

完了後にSystem Rebootする。最初の1回は、アニメーション表示のまま固まって困ったけど、電池を抜いて再度電源を入れたら問題なく起動した。

2013年9月6日金曜日

DialogFragmentでのカスタムダイアログ実装方法

DialogFragmentによるカスタムダイアログ実装方法について、下記の点をまとめておく。お題としてパズドラ風のダイアログを実装してみる。
  • 基本
  • コンテンツ部分
  • スタイル
完成形

基本

DialogFragmentを継承したpublicクラスを作成する。注意点は下記の通り。

  • ファクトリーメソッド(下記例ではnewInstance())を用意する。
  • コンストラクタのオーバーロードを作らない、使わない
public class MyDialogFragment extends DialogFragment
{
  /**
   * ファクトリーメソッド
   */
  public static MyDialogFragment newInstance(String param)
  {
    MyDialogFragment instance = new MyDialogFragment();

    // ダイアログに渡すパラメータはBundleにまとめる
    Bundle arguments = new Bundle();
    arguments.putString("parameter", param);

    instance.setArguments(arguments);
    return instance;
  }
}

コンテンツ部分(お手軽パターン)

コンテンツ部分の実装は、onCreateDialog()やonCreateView()をオーバーライドして行う。 お手軽パターンでは、onCreateDialog()のみをオーバーライドして、必要な機能を有するDialogインスタンスを生成する。

/**
 * ダイアログコンテナを生成する。
 */
@Override
public Dialog onCreateDialog(Bundle b)
{
  // ダイアログのコンテンツ部分
  LayoutInflater i
    = (LayoutInflater) getActivity()
        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  View content = i.inflate(R.layout.mydialog_content, null);

  AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  // タイトル
  builder.setTitle("My Custom Dialog");
  // コンテンツ
  builder.setView(content);
  // OK
  builder.setPositiveButton(android.R.string.ok, null);

  Dialog dialog = builder.create();

  // ダイアログ外タップで消えないように設定
  dialog.setCanceledOnTouchOutside(false);

  return dialog;
}
  • 「お手軽」ではあるが、埋め込み部品として再利用(レイアウトの一部として配置)することができないデメリットがある。
  • HoneyComb以降は、ダイアログ外タップで閉じるのがデフォルトなので注意。

コンテンツ部分(お上品パターン)

お上品パターンでは、onCreateDialog()でダイアログコンテナを生成し、onCreateView()でコンテンツを生成する。OKボタンなどはレイアウトで、ダイアログの見栄えは後述のスタイルで賄う。

/**
 * ダイアログコンテナを生成する。
 */
@Override
public Dialog onCreateDialog(Bundle b)
{
  Dialog dialog = super.onCreateDialog(b);

  // タイトル
  dialog.setTitle("My Custom Dialog");
  // ダイアログ外タップで消えないように設定
  dialog.setCanceledOnTouchOutside(false);

  return dialog;
}

/**
 * UIを生成する。
 */
@Override
public View onCreateView(LayoutInflater i, ViewGroup c, Bundle b)
{
  View content = i.inflate(R.layout.mydialog_content, null);
  return content;
}
  • onCreateDialog()をオーバーライドしつつ、onCreateView()でnull以外を返すとAndroidRuntimeExceptionが発生する…と読めるような情報もあるが、AlertDialog使用方法の問題でありDialogFragment固有の問題では無い (AlertDialogを使わなければ良い)

コンテンツのレイアウト

ここでは、前述のお上品パターンで利用するレイアウト(ボタンも含める)を想定し、下図のようなレイアウトを作成する。RelativeLayoutを利用することで、ダイアログをお好みの位置に表示する。(下記例では画面下部に配置)

このレイアウトのコードは以下の通り。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_margin="10dp"
    android:background="@drawable/dialog_bg"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingBottom="10dp"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="20dp" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"
        android:background="#d595c2d7"
        android:minLines="7"
        android:text="以上の内容で送信してよろしいですか?"
        android:textColor="@android:color/black" />

    <Button
        android:id="@android:id/button1"
        android:layout_width="wrap_content"
        android:layout_height="35sp"
        android:background="@drawable/button_bg"
        android:minWidth="100dp"
        android:text="@android:string/ok"
        android:textColor="@android:color/white"
        android:textSize="22sp"
        android:textStyle="bold" />
    </LinearLayout>
</RelativeLayout>
さらに、ダイアログ背景をXMLで作成する。
drawable/dialog_bg_part1.xml(白枠だけの画像)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <stroke
    android:width="2dp"
    android:color="#ffffff" />
</shape>
drawable/dialog_bg_part2(黒枠にグラデーション塗りつぶしの画像)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <gradient
    android:angle="270"
    android:endColor="#f1114461"
    android:startColor="#f13c91ba" />
  <stroke
    android:width="1dp"
    android:color="#000000" />
</shape>
drawable/dialog_bg.xml(合体)
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
  <item android:drawable="@drawable/dialog_bg_part1"/>
  <item
    android:bottom="2dp"
    android:drawable="@drawable/dialog_bg_part2"
    android:left="2dp"
    android:right="2dp"
    android:top="2dp"/>
</layer-list>
ボタンの背景画像も同様に作成する
drawable/button_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
  <corners android:radius="10dp" />
  <gradient
    android:angle="270"
    android:endColor="#222656"
    android:startColor="#6a75c7" />
  <stroke
    android:width="1px"
    android:color="#0c2446" />
</shape>

スタイル

最後に、背景(前項キャプチャの黒いところ)やタイトル(前項キャプチャのvalueと書いてあるとこ)を消して、ダイアログの表示アニメーションを制御するためにスタイルを定義および適用する。

values/styles.xml
Androidのダイアログテーマを拡張し、タイトル指定・背景・アニメーションの設定を上書きする。

<!-- タイトル無し、背景透明、アニメーション指定 -->
<style name="Theme.MyDialog" parent="@android:style/Theme.Dialog">
  <item name="android:windowNoTitle">true</item>
  <item name="android:windowBackground">@android:color/transparent</item>
  <item name="android:windowAnimationStyle">@style/Animation.MyDialog</item>
</style>

ウィンドウアニメーションスタイルは下記の通り定義する。表示する時のアニメーション(windowEnterAnimation)と消える時のアニメーション(windowExitAnimation)をそれぞれ指定。

<style name="Animation.MyDialog" parent="android:Animation.Dialog">
  <item name="android:windowEnterAnimation">@anim/options_panel_enter</item>
  <item name="android:windowExitAnimation">@anim/options_panel_exit</item>
</style>

アニメーション定義

anim/options_panel_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@android:anim/decelerate_interpolator" >
  <translate
    android:duration="@android:integer/config_shortAnimTime"
    android:fromYDelta="25%"
    android:toYDelta="0" />
  <alpha
    android:duration="@android:integer/config_shortAnimTime"
    android:fromAlpha="0.0"
    android:toAlpha="1.0" />
</set>

anim/options_panel_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
  android:interpolator="@android:anim/accelerate_interpolator" >
  <translate
    android:duration="@android:integer/config_shortAnimTime"
    android:fromYDelta="0"
    android:toYDelta="50%" />
  <alpha
    android:duration="@android:integer/config_shortAnimTime"
    android:fromAlpha="1.0"
    android:toAlpha="0.0" />
</set>

DialogFragmentのonCreate()をオーバーライドして、スタイルを適用するコードを記述する。

/**
 * フラグメント生成コールバックメソッド
 */
@Override
public void onCreate(Bundle b)
{
  super.onCreate(b);

  setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_MyDialog);
}

おしまい


参考サイト

2012年10月15日月曜日

AndroidでParse(Baas)を使ってPush通知する

巷で流行のBaaS(Backend as as Service)に、Parseというのがある。
色々なことができるけど、現状では「各種APIとデータベースを兼ね備えたサーバーアプリケーション」という風に認識され、使われていることが多いみたい。

今回はそんなParseの機能の一つであるNotification(Push 通知)を使って、サーバーサイドの開発無しにPush通知を実装してみる。

ちなみに100万リクエスト/月、1GBストレージまでは無料で使える。

ユーザー登録


www.parse.comへアクセスし、Try it freeボタンからサクっとユーザー登録を行う。

Quick Start

登録が済んだらログインする。自動生成とかスケルトンとかに抵抗がなければそのままQuick Start Guideへ進むと下記のページになる。



Choose your platformでAndroidを選択し、未作成であればCreate an Appあたりからアプリケーション作成を行う。
さらにBlank Android project with Parse SDK(.zip)というリンクから空のAndroidプロジェクトファイルをダウンロードする。Eclipseでダウンロードしたzipファイルを既存のプロジェクトとしてインポートする。

Quick Startの中身

自動生成とかスケルトンとかに抵抗がある場合、何を自動生成しているのか不安でいっぱいになるので、何が起きているのかを簡単にまとめておく。

  1. 空のAndroidプロジェクトを作成
  2. SDK(zipファイル)をlibsディレクトリへ設置
  3. プロジェクトのライブラリエクスポート設定に上記SDKを追加
  4. Applicationクラスを継承した新規クラスを作成し、onCreateメソッドをオーバーライド。さらにParse.initialize()メソッドの呼び出しを追加
  5. AndroidManifest.xmlのapplicationタグにandroid:name="作成したAppicationクラス名"を追記

といったことをやったのと同じ。


AndroidManifest.xmlの設定

AndroidManifest.xmlにサービスとブロードキャストレシーバの設定を追加する。

<!-- サービスを登録 -->
<service android:name="com.parse.PushService" />

<!-- ブロードキャストレシーバを登録 -->
<receiver android:name="com.parse.ParseBroadcastReceiver">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <action android:name="android.intent.action.USER_PRESENT" />
  </intent-filter>
</receiver>
パーミッションの利用設定を追加する。 ※必要最低限のみなので公式ドキュメントのチュートリアルとは少し異なる ※公式ドキュメントには書いていなかったけど、ACCESS_NETWORK_STATEも必要だった
<uses-permission
  android:name="android.permission.INTERNET" />
<uses-permission
  android:name="android.permission.ACCESS_NETWORK_STATE"/>

参考 : Push Notifications - Parse.com



Application IDとClient Keyの設定

Parseサイトにログインした状態で、アプリケーションのOverviewページを開くと、Application IDとClient Keyが表示されるのでこれをメモしておく。

ParseApplicationクラスのonCreateメソッドに記載されている、Parse.initializeの引数を上記の内容に書き換える。

Push通知を受け取るためのコードの追加

前項同様ParseApplicationクラスのonCreateメソッドに下記のコードを追加する。

PushService.subscribe(this, "", ParseStarterProjectActivity.class);

Push通知送信

Parseサイトにログインした状態で、アプリケーションのPush Notificationsページを開くと簡単なPush通知送信管理画面が開くので、適当なメッセージを記入してボタンを押すだけ。


REST APIでPush通知を行う

ParseサイトでのPush通知は、REST APIで以下のリクエストを送信しているっぽい。
送信する内容と受け側のアプリをカスタマイズすれば「Push通知をステータスバーに表示する」というデフォルト動作を変更することができる。

Header

項目値
URLhttps://api.parse.com/1/push
MethodPOST
X-Parse-Application-IdアプリケーションID
X-Parse-REST-API-KeyREST APIキー
Content-Typeapplication/json

Body

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "alert": "This is test message."
  }
}

Push通知で独自の動作を実装する



配信側

REST APIへ送信するJSONデータのalertをactionに変更する。(以下例)

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "action": "jp.blogspot.tomokey.action.SAMPLE",
    "msg": "This is test message."
  }
}

受信側

ブロードキャストレシーバを作成し、インテントフィルタのactionに上記のjp.blogspot.tomokey.action.SAMPLEを指定する。ブロードキャストレシーバの実装は、以下のようにcom.parse.DataをキーとしてJSONデータを取得すれば、あとはお好みの処理を。

public class MyReceiver extends BroadcastReceiver
{
  public void onReceive(Context ctx, Intent intent)
  {
    try
    {
      // データを取得
      Bundle extra = intent.getExtras();
      String data = extra.getString("com.parse.Data");
      
      // jsonオブジェクトへパース
      JSONObject json = new JSONObject(data);
      
      String msg = json.getString("msg");
      Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show();
    }
    catch (JSONException e)
    {
      e.printStackTrace();
    }
}

念のためブロードキャストレシーバの定義も。

<receiver android:name="MyReceiver">
  <intent-filter>
    <action android:name="jp.blogspot.tomokey.action.SAMPLE"/>
  </intent-filter>
</receiver>

2012年5月5日土曜日

ノートPC(G570)へのUbuntuインストールメモ

LenovoのノートPC(G570)へUbuntuをインストールした際のメモを残しておく。

無線LANを使えるようにする


Ubuntu 12.04の場合

  1. STAドライバをアンインストールする(パッケージマネージャーから下記を検索してアンインストール)
    bcmwl-kernel-source
    
  2. 以下のツールをインストールする(パッケージマネージャーで検索してインストール)
    firmware-b43-installer(これはいらないかも)
    b43-fwcutter
    
  3. blacklist.confを修正
    # sudo vim /etc/modprobe.d/blacklist.conf
    blacklist bcm43xxという行をコメントアウトする
    
  4. マシンを再起動

Ubuntu 11.04の場合

  1. デバイスを確認する
    # lspci -v | grep Broadcom -A 4
    02:00.0 Network controller: Broadcom Corporation BCM4313 802.11b/g/n Wireless LAN Controller (rev 01)
    Subsystem: Broadcom Corporation Device 051b
    Flags: bus master, fast devsel, latency 0, IRQ 17
    Memory at d0400000 (64-bit, non-prefetchable) [size=16K]
    Capabilities: 
    Kernel modules: brcm80211
    
  2. ドライバモジュールを読み込ませる
    # modprobe brcm80211
    
  3. 起動時にドライバモジュールを読み込むように設定する
    # sudo vim /etc/modules
    brcm80211を追記
    

Ubuntu 10.10の場合

  1. デバイスを確認する
    # lspci | grep Broadcom
    02:00.0 Network controller: Broadcom Corporation BCM4313 802.11b/g/n Wireless LAN Controller (rev 01)
    
  2. ここからドライバソースをダウンロードする
  3. ドライバをインストールする
    # tar xvzf hybrid-portsrc_x86_32-v5_100_82_112.tar.gz
    # make && make install
    
  4. ドライバモジュールを読み込ませる
    # depmod
    # modprobe wl
    
  5. 起動時にドライバモジュールを読み込むように設定する
    # sudo vim /etc/modules
    wlを追記
    

インストール後の設定


ユーザディレクトリを英語表記へ変更する

# LANG=C xdg-user-dirs-gtk-update

タッチパッドを無効化する

  1. 確認
    # xinput list | grep TouchPad
    
  2. 無効化(デバイスIDは適宜変更)
    # xinput set-prop 13 "Device Enabled" 0
    

JDK 7のインストール

  1. ダウンロードして展開(ここからダウンロードする。)
  2. コピー
    $ sudo mkdir /usr/lib/jvm
    $ sudo cp -R jdk1.7.0_03 /usr/lib/jvm/
    
  3. update-javaツールインストール
    $ sudo add-apt-repository ppa:nilarimogard/webupd8
    $ sudo apt-get update
    $ sudo apt-get install update-java
    
  4. java実行環境設定
    $ sudo update-java
    

環境

購入したもの

Lenovo G570 4334C3J ¥37,000
SODIMM DDR3-1333(PC3-10600) 4GB x 2枚 ¥3,200

スペック

Size15.6インチ
CPUCore i3 2.2GHz(Dual Core)
MemDDR3-1333(PC-10600) 8GB
HDDSeagate ST9500325AS 500GB
DVDHLDS GT50N DVD±RW
LANAtheros AR8152
WLANBroadcom BCM4313 802.11b/g/n

2012年5月2日水曜日

Visitorパターン

Visitorパターンは、ツリー構造のデータ群(ディレクトリ構造を含めたファイル群みたいな)を処理する際に役立つ設計パターン。別にツリー構造に制限される訳ではないけど、実際それ以外で使わない気がする。

重要な登場人物

Element
データ構造の各要素を表すクラス。ファイルに当たるもの。
ObjectStructure
Elementの集合を扱うクラス。ディレクトリに当たるもの。
Visitor
データ構造の各要素を利用した処理の実装。ElementとObjectStructureに対して行う処理を実装する。


Visitorパターンでは、データ構造の階層の深さを意識することなく、またデータ構造側にデータ処理を一切書くことなく走査を行うことができる。データ走査は、Visitorをroot要素に渡すだけ。あとは勝手にデータ構造を舐め回して各Element(i.e ファイル)へ到達してくれる。


Visitorの実装

各要素に対して処理を行うためのAPIのみを定義した抽象クラスを定義する。

abstract void visit(ObjectStructure aDir);
abstract void visit(Element aFile);

具体的な処理内容に依ってVisitorの継承クラスを実装する。例えば処理内容が「ファイルの名前一覧を作成する」であった場合、FilenameVisitorクラスを作成して、次のような実装を行う。

/**
 * ディレクトリに対して行う処理を定義。
 * @param aDir ObjectStructure
 */
public void visit(ObjectStructure aDir)
{
  for (Element each : aDir.children())
  {
    each.accept(this);
  }
}

/**
 * ファイルに対して行う処理を定義。
 * @param aFile Element
 */
public void visit(Element aFile)
{
  logger.print(aFile.getName());
}

ObjectStructureの実装

Elementの集合を扱うクラスであり、以下のような実装になる。データ処理に関する実装は一切必要ないところがポイント。

/**
 * 子要素のリストを取得する。
 * @return 子要素リスト
 */
public List<Element> children()
{
  return this.children;
}

/**
 * データ処理インターフェースの受け口。
 * @param aVisitor Visitor
 */
public void accept(Visitor aVisitor)
{
  aVisitor.visit(this);
}

Elementの実装

最小の要素を示すクラスであり、以下のような実装になる。これもデータ処理に関する実装は一切必要ない。

/**
 * 名前を取得する。
 * @return 名前
 */
public String getName()
{
  return this.name;
}

/**
 * データ処理インターフェースの受け口。
 * @param aVisitor Visitor
 */
public void accpt(Visitor aVisitor)
{
  aVisitor.visit(this);
}


Bridgeパターン

今回はBridgeパターンを復習してみる。Bridgeパターンは文字通り橋渡しをするパターンであり、まずは機能と実装を分離することの大切さを思い出す必要がある。

具体的なケースとして以下が考えられる。

任意の処理を行うバッチプログラムを作成するためのクラス設計


基本クラス
すべてのバッチプログラムに共通の処理(ログ出力)を実装し、ビジネスロジックについては各継承クラスに任せる。
継承クラスA
ビジネスロジックとしてAという機能を実装
継承クラスB
ビジネスロジックとしてBという機能を実装

変更要求

このクラス設計にしたがって開発を行った場合、次のような変更要求があると途端にクラス設計の美しさが損なわれる。
  • CおよびDという機能を実現するバッチを追加してほしい。また、これらのバッチは起動時にあるファイルをロックし、終了時にロックを解除する必要がある。

要求への対応

基本クラスへロック機能を追加すると、既存のプログラムに影響がでてしまうので下記いずれかが対応として考えられる。
  1. 基本クラスにロック機能を追加した中間クラスを作成し、継承クラスCとDではこの中間クラスを継承する。
  2. ロック機能は別クラスに集約し、継承クラスCとDでは集約クラスのロック機能を利用する。

対応案の考察

  • 1番目の方法は、基本クラスから始まる「実装を追加するための継承」という流れに「機能を追加するための継承」という流れを混ぜてしまう。
  • 2番目の方法は、ロック機能とビジネスロジックの結合が強くなりがち。(やっぱロック機能いらないや、とか別のバッチにもロック機能つけて、とか言われた場合にビジネスロジックを含むクラスの修正が必要になってしまう)

さらに、Eという機能を実現するバッチを追加してほしい。また、このバッチの実行開始と終了時にEメールを送信してほしい。などと言われると余計面倒なことになる。

機能と実装の分離

このような場合に、Bridgeパターンに基づいて機能(を追加するための継承)と実装(を追加するための継承)を別の流れになるようなクラス設計にしておけば、変更要求に強い構造になる。
Abstraction
機能を追加する流れの最上位クラス。Implementorのインスタンスを保有する(=Bridge担当)
Implementor
実装を追加する流れの最上位クラス
Refined Abstraction
機能を追加する際にAbstractionを継承して作成するクラス。例で言うところのロック機能の追加はここで実装する。
Concrete Implementor
実装を追加する際にImplementorを継承して作成するクラス。例で言うところの継承クラスA~Dはこれのこと。

Mementoパターン

たまにはデザインパターンについて復習でもしてみる。今回はMementoパターン。
Mementoには記念品や思い出といった意味があり、ざっくりとまとめると「状態を保存しておけば、やり直しができるじゃないか!」といったもの。

Mementoパターンに登場するのは下記の3クラス。(便宜上クラスとしているが、役という表現のほうがしっくりくるかも)

Originator

Save(Mementoを作成する)とLoad(Mementoを読み込む)の機能を有するクラス。

Memento

セーブデータそのものを表すクラス。

Caretaker

Originatorに働きかけてSaveとかLoadを実行し、セーブデータ(Memento)を管理するクラス。


ポイント

  • Originator君はMementoを作成する人なので、Mementoのすべてに触れることができる。
  • 対してCaretakerさんはMementoを管理(永続化とか)するだけなので、Mementoの必要最低限についてのみ触れられれば良い。
って感じにすると、Mementoが美しくカプセル化される。

2011年10月26日水曜日

Cookieを使ってもいいの?

Cookieといえば、ログインを伴うサイト構築を行う際に、セッションIDを保存するアレだ。
しかし、なんとなく使っているCookieなので、セキュリティとかどうなの?とかHTML5のWeb Storageと比べてどうなの?など、突き詰めるとモヤっとした部分が多い。

今回は、とりあえずCookieの使用を検討するにあたって知っておくべきと思われることをまとめる。

Cookieとは

複数のページを跨いだ、あるいはブラウザの起動を跨いだ情報共有を行うための機構である。 その実態は、PC上に保存されたテキストファイルであり、ページにアクセスする度にサーバーへ送信される。

Cookieのパラメータ

パラメータ意味
KEY=VALUECookieに設定するキーと値
expires=VALUE有効期限、省略=ブラウザ終了まで、過去=Cookie削除
domain=VALUECookieの送信先ドメイン
path=VALUECookieの送信先パス
secureSSL通信時のみCookieを送信する

Cookieの発行方法


その1:HTML
<meta http-equiv="Set-Cookie" content="VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;">

その2:JavaScript
document.cookie
  ="VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;";

その3:サーバーサイド

HTTPヘッダで指定する

Set-Cookie: VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;

Cookieにまつわるセキュリティの話


Domainパラメータ

ockeghem(徳丸浩)の日記によれば、RFC6265に規定されている通り、Cookieの送信先ドメインはDomainパラメータで指定したドメインおよびそのサブドメインであるらしい。
送信先を限定するという意味では、Domainパラメータを指定しない(=発行元ホストのみに限定する)ほうが安全である。

但し、IE9、iモードではDomain指定無しでもRFC6265を無視してサブドメインへも送信してしまうので注意が必要。


Pathパラメータ

PathパラメータではCookieの送信先パスのプレフィクスを指定できるが、その効果は「無用なCookie送信を抑制する」くらいであり、「指定したパス以外にCookieが送信されないことを保証」できるものではない。

高木浩光@自宅の日記に詳細が記されているが、同一ドメイン内の任意のページでFRAME+JavaScriptを利用することでCookieを取得できてしまう。


Cookie Monster Bug(Cross Domain Cookie Injection)

  • .example.co.jpに対して発行したCookieは、そのサブドメインであるwww.example.co.jpにも送信される。
  • 同様に.co.jpに対して発行したCookieは、あらゆるxxx.xxx.co.jpに対して送信される。

故に.co.jpや.comに対してCookieが発行されないようにブラウザが規制すべきだが、IE9やOpera 11にはこの規制が無い…という問題。
(参考:属性型JPドメインと地域型JPドメインに対するCookie Monster Bug調査)

典型的な影響としてセッションIDの固定化攻撃(Session Fixation)というのがあるらしい。奴らはこんな感じで攻めてくる。


Session Fixation攻撃への対策

  • ログインの度にセッションIDを再発行して、ちゃんとCookieに設定し直す。


Cookie Monster Bugへの対策

Cookie Monster Bugの影響により、上位のドメインに対して発行されたCookieが干渉する可能性がある。一例として下記が挙げられる。

SESSIDという名前で2つの値が送信されているHTTPリクエスト

POST /login.cgi HTTP/1.0
(略)
Cookie: SESSID=malicious_cookie; SESSID=08afa677654dcbg44eadfb46e1858119
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

対策として

  • 上位のドメインに対して有効期限切れのCookieを発行して無効化する
  • 複数SESSIDへの対応を行う(有効なものを探す or エラーとしてしまう)
  • Cookie使わない
といったことが考えられる。
参考:Cookie Monster襲来! 戦え、星野君

2011年7月5日火曜日

Honeycomb(Android 3.0)まとめ(その2)

前回に引き続き、7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。

今回はプログラミングTipsについて。

互換性

parallel activities pattern

Honeycomb(Android OS 3.0)以上であるか、タブレット端末であるか、従来のスマートフォン(Android OS 2.x)であるかによってActivityを切り替えるTips

// OSバージョンを判定する方法
boolean isHoneycomb = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;

// さらに画面サイズを判定する方法
int layoutSize
  = context.getResources().getConfiguration().screenLayout
  & Configuration.SCREENLAYOUT_SIZE_MASK;

boolean isTablet = layoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE;


Interfaceでセンサーを隠蔽

例として画面の傾きを取得するための手段を提供するinterfaceを定義し、その実装としてデバイスの搭載センサーに応じてジャイロスコープセンサーと加速度センサーを切り替えるといった方法

// 傾きを取得するための手段を提供するinterface
interface IOrientationSensorListener
{
  public String getValue();
}

ジャイロスコープセンサーを利用する実装

// ジャイロスコープセンサーを利用する実装
class Gyro implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Gyro(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_GYROSCOPE)
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}

加速度センサーを利用する実装

// 加速度センサーを利用する実装
class Accelerometer implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Accelerometer(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), // ★
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) // ★
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}
※飽くまでコンセプト。上記くらいの差異なら実装は一つにして、初期化パラメータでジャイロと加速度センサーを振り分けたほうが見通しが良さそう。

サポートしているフィーチャーの確認方法

一応。
PackageManager pm = getPackageManager();

// 加速度センサーのサポート状況
boolean supported
  = pm.hasSystemFeature(
      PackageManager.FEATURE_SENSOR_ACCELEROMETER);


画面の向きはPortraitが標準ではない

Tipsとは少し違うけど、タブレットで顕著になった画面の向きという要素に気をつける。Android主要端末の画面サイズ(small, normal, large, xlarge)でも触れたように、layoutのxmlを格納するディレクトリをport用およびland用に作成して対応すべき。



トラッキング

ユニークなインストール数の取得や、特定ユーザーの挙動をトラッキングする目的で使用できる識別子は2種類ある。また、前提として…

  • 工場出荷初期化を行っても変化しない値は使用すべきではない
  • それなりに高い精度でのトラッキングが目的であり、厳密に個人を識別する値と考えてはいけない


その1

工場出荷初期化を行うとクリアされる値であり、Root権限を取得している端末では書き換え可能な値なので個人を識別するものとして利用してはいけない。似ているけどSettings.System.ANDROID_IDではないので注意。

Settings.Secure.ANDROID_ID;


その2

数学的に重複する可能性が低いとみなすことのできるユニークな値。

UUID.randomUUID().toString();


Location Based Service

位置情報サービスを利用する際には常に電力消費の大きさが問題になる。また、位置情報を取得するまでの時間的なイニシャルコストも同様である。これらの問題を解決するためのTips

  • 前回取得した位置情報を活用する
  • 位置情報取得を1度だけ行う
  • 適切な頻度で位置情報をトレースする
  • 位置情報プロバイダの生死をハンドリングする
  • バックグラウンドで位置情報を取得しておく


前回取得した位置情報を活用する

利用可能な位置情報プロバイダが保有している位置情報を検査し、一定の条件を満たす有効なデータがあればこれを利用する。

List<String> providers = locationManager.getAllProviders();
for (String provider: providers)
{
  Location location
    = locationManager.getLastKnownLocation(provider);
  
  // locationの時刻を一定の閾値でテストして有効なものを利用する
  ...
}


位置情報取得を1度だけ行う

位置情報を追いかけ続ける必要性がなければ、可能なかぎり素早く現在の位置情報を取得するといった方法を採る。※但し、criteria(現在のデバイス状況など)に従って決定されるため、位置情報プロバイダは必ずしも素早いとは限らない。

locationManager.requestSingleUpdate(criteria, singleUpatePI);

また、上記はGingerbreadより前のAndroid OSでは利用できないため代替手段などを使う必要がある。



適切な頻度で位置情報をトレースする

例えば、頻度は毎15分、75m以上の移動があった場合など。

locationManager.requestLocationUpdates(
  AlarmManager.INTERVAL_FIFTEEN_MINUTES,
  75,
  new Criteria(),
  pendingIntent);


位置情報プロバイダの生死をハンドリングする

使用中のプロバイダが使えなくなった場合や、より良いプロバイダが利用可能になった場合についてブロードキャストレシーバを登録してハンドリングする。

// 位置情報プロバイダが利用できなくなったよーブロードキャストを
IntentFilter intentFilter
  = new IntentFilter(
      PlacesConstants.ACTIVE_LOCATION_UPDATE_PROVIDER_DISABLED);

// ハンドリングするレシーバを登録
registerReceiver(
  locProviderDisabledReceiver,
  intentFilter);


バックグラウンドで位置情報を取得しておく

UIの起動と同時に位置情報に依存した挙動を行う必要性がある場合には、サービスにおいて位置情報の取得を行っておく。但し、電力消費を考慮しFroyo(Android 2.2)以降では、Passive Providerを利用する。

locationManager.requestLocationUpdates(
  LocationManager.PASSIVE_PROVIDER,
  minTime,
  minDistabce,
  pendingIntent);

Honeycomb(Android 3.0)まとめ(その1)

7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。ちなみに、内容は前日に行われたGoogle Android Developer Lab Tokyo 2011と同様らしい。

概要

  • SDKバージョンは11
  • Fragmentsなどの機能についての互換性は静的ライブラリの提供で保つ(Android Compatibility package)
  • Honeycombで追加したUIや機能は、スマートフォン版(2.x系)へ適用していく(Icecream Sandwich)

New Features

Honeycombで追加された新要素達は、こんな感じ。

  • Hardware Acceleration
  • Drag & Drop
  • Multi choice List
  • Bluetooth APIS
  • System wide clipboard
  • RenderSript
  • Loaders
  • App Widgets

ユーザーインターフェース特性

システムバー

Honeycombにおいて、画面最下部に常時表示されているインターフェースをシステムバーと呼称する。
ハードウェア的なボタンが存在しないタブレット端末では、ソフトウェア的にこのシステムバーを描画することにより、2.xと酷似したインターフェースを提供する。

システムバーの表示制御は下記のAPIで行うことができるが、描画領域を消す手段は提供されていない。ユーザーが混乱する(ホーム画面に戻る手段がなくなる等)ため必要な場合以外は常に表示することが推奨される。

View#SetSystemUiVisibility( STATUS_BAR_VISIBLE or STATUS_BAR_HIDDEN )

システムバーの右端には通知アイコンが表示される。通知アイコンをタップすることにより表示されるインターフェースが大幅に強化されているため、画像にあるようなメディアプレイヤー機能を通知領域のみで実現するといったことが可能になっている。



アクションバー

Honeycombにおいて、画面最上部に表示されているインターフェースをアクションバーと呼称する。
Android 2.xまでは、各デベロッパーが画面上部に配置するインターフェースを独自に実装してきた。

Honeycombでは多様なニーズに答えつつ統一されたユーザー体験を提供できるようにプラットフォームで画面上部のインターフェース(=アクションバー)を提供する。



Drag & Drop

大きな画面を有するタブレット端末ならではのインターフェースとして、Drag & Dropを標準APIとしてサポートする。



設計

Fragments

機能の集合体をFragmentと呼称し、この集合体の組み合わせによって様々なデバイスへの対応が可能になる。



見た目のデザイン

RenderScript

RenderScriptを利用するとハイパフォーマンスな3D描画処理をJNI無しで実装可能。



ハードウェアアクセラレーション

LiveWallpaperなどで利用されているGPUを使用した描画高速化を、通常のGUI部品にも適用できるようになった。

android:hardwareAccelerated


アニメーション

少量のコードでViewのプロパティを用いたアニメーションを行うことができる。

ObjectAnimator.ofFloat(myView, "alpha", 0f).start();

2011年6月26日日曜日

Java SE 7で何が変わるのか

来る2011年7月28日は、5年ぶりのJavaアップデート予定日。次期バージョンのJava SE 7にはいくかの新しい要素が含まれているので、先日のJJUG Cross Community Conferenceの振り返りを兼ねてまとめておく。

Java SE 7の主なトピックは以下の通り。

  • Project Coin (JSR 334)
  • NIO.2(JSR 203)
  • Fork/Join Framework
  • Invoke Dynamic (JSR292)

Project Coin

Project Coinとは、言語仕様な小さな変更を行うプロジェクト。

switch文でStringが使えるようになる

public void execute(String state)
{
  switch (state)
  {
  case "ready":
    startSomething();
    break;

  case "running"
    finishJob();
    brak;
  }
}

数値表現形式の追加

バイナリ表記(0bで書き始める)が追加された。

  • 1 (10進数表記)
  • 01 (8進数表記)
  • 0x1 (16進数表記)
  • 0b1 (バイナリ表記)

可読性向上を目的としたアンダースコア表記が追加された。

// クレカ番号など?
long data = 1234_5678_9012L;

マルチキャッチ

数種の例外処理をまとめることが可能になった。

try
{
  doSomething();
}
catch (IOException | SqlException e)
{
  e.printStackTrace();
}

例外再送の記述を簡易化

catch句にfinalを付けることで、メソッド定義のthrows部が不要になる。

public void execute() // throws IOException が不要
{
  try
  {
    doSomething();
  }
  catch (final IOException e)
  {
    throw e;
  }
}

Genericsインスタンス生成時の型推論強化

右辺では<>のみ記載すればOKになった。

// 今までの書き方
Map<String, String> data = new HashMap<String, String>();

// これからの書き方
Map<String, String> data = new HashMap<>();

try構文におけるリソース解放機能の追加

C#で言うところのusing的な感じ。自動的にclose()メソッドがコールされる。

try ( InputStream is = new FileInputStream("/tmp/data.txt") )
{
  doSomething();
}
catch (IOException e)
{
  e.printStackTrace();
}

Android主要端末の画面サイズ(small, normal, large, xlarge)

「drawable-mdpi」や「layout-normal」とか、時間が無い開発業務の中では、気にしたら負けかなと思う。


…なんてこともあるかもしれないけど、こういったところを気にしないと、アプリが安っぽく見えたり、機種によって使いやすさが違いすぎるといったことが発生する。

先に挙げたのは、普段から何気なく使っているリソースディレクトリのことであり、何となく名前から想像がつく通り、画面サイズや解像度に応じて別々のリソース(画像やら画面レイアウトやら)を格納しておくことができる。細かいことはあんざい先生の本に任せて、今回はざっくりした説明といくつかの主要端末についての表を主題とする。

ざっくり

レイアウトファイルを格納するディレクトリは、以下を使い分ければ大概OK(例:layout-normal-long-port)

  • small, normal, large, xlarge(Androidで独自に定めた基準)
  • long, no-long(横長か否か)
  • port, land(画面が縦の時、横の時)

画像ファイルを格納するディレクトリは、以下を使い分ければOK

  • ldpi, mdpi, hdpi, xhdpi

主要端末の画面サイズ表

手元に情報がある端末について、計算でそれぞれのAndroid OSにおけるサイズ情報を算出し表にしてみた。

モデルサイズ解像度
SO-01B(Xperia)normalhdpi
SO-01C(Xperia arc)normalhdpi
SO-02C(Xperia acro)normalhdpi
IS03normalxhdpi
IS04normalhdpi
SHI05(IS05)normalhdpi
SC-02B(Galaxy S)normalhdpi
SC-02C(Galaxy S II)normalhdpi
N-04C(MEDIAS)normalhdpi
N-06C(MEDIAS WP)normalhdpi
L-07C(Optimus bright)normalhdpi
X06HT(Desire)normalhdpi
HTC Legendnormalmdpi
MZ604(Xoom wifi)xlargemdpi
L-06C(Optimus Pad)xlargemdpi

但し、注意点として同じnormalであっても、微妙にサイズが異なるので注意が必要。

320x569(単位はdip)

  • SO-01B(Xperia)
  • SO-01C(Xperia arc)
  • SO-02C(Xperia acro)
  • IS04
  • SHI05(IS05)
  • N-06C(MEDIAS WP)

320x533(単位はdip)

  • SC-02B
  • SC-02C
  • X06HT
  • L-07C

320x480(単位はdip)

  • IS03
  • HTC Legend

※解像度からしてもIS03は、日本で発売されているAndroid端末の中では特殊な機体と言える。

スペック情報から該当するサイズ情報を割り出す

簡単に端末スペックから該当するサイズ情報を算出するフォームを作ってみた。

入力

Width:px
Height:px
Density:(ex 1.0~2.0)
Density Dpi:

結果

Width:dip
Height:dip
画面サイズ:
解像度:

2011年6月6日月曜日

PHPでOAuthをやっつける方法

前々回のOAuthについてまとめてみた、前回のJavaでOAuthをやっつける方法に続いて今回はPHPでOAuth認証を利用したAPIへのアクセスを実装してみる。

今回も、天下のoauth.netのoauthライブラリ(Google Code)を使う。oauthのリポジトリからOAuth.phpを持ってくれば準備完了。


すぐ使いたい時

Java版と併せてライブラリにまとめてみたのでここ(Google Code)から取得してもらうと、簡単に実装できる。HowToUse.phpを見てもらうのが手っ取り早いけど、How to use for phpにも触りだけ書いてあるのでよかったら見てください。


やること

  • 各種の設定を定義
  • リクエストトークンを取得する
  • ユーザーにアプリケーションを承認してもらう
  • アクセストークンを取得する

各種の設定を定義

サービスプロバイダーのエンドポイント情報やコンシューマの情報を、下記のように定義する。

$TOMOKEY = array(
  'CONSUMER_KEY' => 'anonymous',
  'CONSUMER_SECRET' => 'anonymous',
  'CONSUMER_CALLBACK_URL' => 'oob',
  'REQUEST_TOKEN_URL'
    => 'https://www.google.com/accounts/OAuthGetRequestToken',
  'USER_AUTHORIZATION_URL'
     => 'https://www.google.com/accounts/OAuthAuthorizeToken',
  'ACCESS_TOKEN_URL'
     => 'https://www.google.com/accounts/OAuthGetAccessToken'
);
※Goole Data APIをクライアントアプリから利用する例なので、ウェブアプリの場合とかはJavaでOAuthをやっつけるを参照。

リクエストトークンを取得する

Step1.コンシューマ情報をオブジェクト化

// consumer info
$consumer_key
  = $consumer_config['CONSUMER_KEY'];
$consumer_secret
  = $consumer_config['CONSUMER_SECRET'];
$consumer_callback
  = $consumer_config['CONSUMER_CALLBACK_URL'];

// OAuthConsumerオブジェクトを構築
$consumer
  = new OAuthConsumer(
      $consumer_key,
      $consumer_secret,
      $consumer_callback);
Step2.署名ツールを生成
// 署名方式はHMAC-SHA1
$signature_method = new OAuthSignatureMethod_HMAC_SHA1();
※今回はよく使うSHA-1方式にしとく。
Step3.パラメータを生成
$params
  = array(
      'oauth_callback'
        => $consumer->callback_url,
      'scope'
        => 'https://www.google.com/calendar/feeds/');
※Google Calendar APIにアクセスする例なので、scopeというパラメータも設定してる。
Step4.リクエストオブジェクトを生成
$request
  = OAuthRequest::from_consumer_and_token(
      $consumer,
      NULL, // RequestToken取得時は不要
      'GET',
      $TOMOKEY['REQUEST_TOKEN_URL'],
      $params);

// 署名する
$request->sign_request($signature_method, $consumer, NULL);
※先につくっておいたOAuthConsumerオブジェクト、パラメータを使ってリクエストオブジェクトを生成し、署名ツールで署名を行う。
Step5.HTTP通信を行う。
// cURLリソースの生成
$ch = curl_init();
// Locationヘッダは無視
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
// サーバ証明書の検証を行わない
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// レスポンスを文字列として取得する設定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// (出力結果に)ヘッダを含めない
curl_setopt($ch, CURLOPT_HEADER, false);
// URLを指定
curl_setopt($ch, CURLOPT_URL, $request->to_url());

// 実行
$result = curl_exec($ch);

// close curl resource to free up system resources
curl_close($ch);

// レスポンスを解析
parse_str($result, $result_map);

// リクエストトークンを取得
$req_token
  = new OAuthToken(
      $result_map['oauth_token'],
      $result_map['oauth_token_secret']);
※POSTで送信する場合には、CURLOPT_URLには、to_url()の代わりにget_normalized_http_url()を使うのと、CURLOPT_HTTPHEADERにarray($request->to_header)ってな感じでAuthorizationパラメータ設定が必要。その他の詳細はコード(httpという名称のfunction)を参照。

ユーザーにアプリケーションを承認してもらう

下記のように作成したURLに対して、ブラウザでアクセスしてもらう。PHPならheaderでリダイレクトするのが一般的なのかな?

// サービスプロバイダーが定義しているエンドポイント
$auth_url = $TOMOKEY['USER_AUTHORIZATION_URL'];

// ここで作成したURLにアクセスしてもらう
$url
  = $auth_url .
    '?' .
    OAuthUtil::build_http_query(array('oauth_token' => $req_token->key));
※承認画面が表示されて、ポチっとした後はcallback URLにリダイレクト or 画面にverifier表示。

アクセストークンを取得する

Step1.色々

リクエストトークンの取得と同じ手順でコンシューマ情報のオブジェクト化と署名ツールの生成を行う。 ちなみに、取得しておいたリクエストトークンは、この後使うのでセッションとかデータストアとかを使って保存しておく必要がある。
Step2.パラメータを生成
$params
  = array('oauth_verifier' => $verifier);
※ここで使用している$verifierは、ユーザー承認により取得した文字列のこと。callback URLが指定されているなら、リダイレクト先にoauth_verifierとして送信される。
Step3.リクエストオブジェクトを生成
$request
  = OAuthRequest::from_consumer_and_token(
      $consumer,
      $req_token, // 保存しておいたリクエストトークンオブジェクト
      'GET',
      $TOMOKEY['ACCESS_TOKEN_URL'],
      $params);

// 署名する
$request->sign_request($signature_method, $consumer, $req_token);
※リクエストトークンの取得時と異なるのは3点。
  • URLがACCESS_TOKEN_URL
  • リクエストトークンを送信する
  • 署名鍵としてリクエストトークンのシークレットの方も利用する
Step4.HTTP通信を行う。
この手順は前述と同じ。最後に取得できるのがアクセストークンってとこだけ違う。

終わり

アクセストークンを使ってAPIアクセス方法は、アクセストークンの取得手順でリクエストトークンの代わりにアクセストークンを指定した場合と同じ。
だけど、リダイレクトの考慮をしないとGoogle Calendar APIとかは使えないので、コード(invokeという名称のfunction)をご参照あれ。

2011年6月3日金曜日

JavaでOAuthをやっつける方法

前回、OAuthについてまとめてみたので、今度は実際にOAuth認証を利用したAPIへのアクセスをJavaで実装してみる。

今回は、天下のoauth.netのoauthライブラリ(Google Code)を使う。

oauthのリポジトリからcommons、consumer、httpclient4を持ってくる。

OAuthのライブラリはいくつかあるみたいだけど、Javaのコードを書く場合はAndroidアプリやGoogle App Engineなどにも移植可能なものがよさげ。
(Jakarta Commons HttpClient v4にてOAuth認証を組み込むとコードがすっきりする。一方、GAEではHttp Client v4が使えなかったりする。こういった違いを吸収できるとこがイイ)


やること

いっぱい。。。

  • consumer.propertiesを用意
  • consumer.propertiesを読み込む
  • リクエストトークンを取得する
  • ユーザーにアプリケーションを承認してもらう
  • アクセストークンを取得する

consumer.propertiesを用意

まず、サービスプロバイダーのエンドポイント情報やコンシューマの情報を、下記のようなconsumer.propertiesファイルに定義する。(適当な名前を付ける。例ではtomokey)

tomokey.consumerKey: consumer_key
tomokey.consumerSecret: consumer_secret
tomokey.callbackURL: oob
tomokey.serviceProvider.baseURL: https://www.google.com/
tomokey.serviceProvider.requestTokenURL: /accounts/OAuthGetRequestToken
tomokey.serviceProvider.userAuthorizationURL: /accounts/OAuthAuthorizeToken
tomokey.serviceProvider.accessTokenURL: /accounts/OAuthGetAccessToken
この設定内容は、各サービスプロバイダーや利用形態で異なる。

Google Data APIをWebアプリケーションから利用する場合

consumer key発行されたもの
consumer secret発行されたもの
callback URLverifierを受け取るURL
request token URLhttps://www.google.com/accounts/OAuthGetRequestToken
user authorization URLhttps://www.google.com/accounts/OAuthAuthorizeToken
access token URLhttps://www.google.com/accounts/OAuthGetAccessToken

Google Data APIをクライアントアプリケーションから利用する場合

consumer keyanonymous
consumer secretanonymous
callback URLoob
request token URLhttps://www.google.com/accounts/OAuthGetRequestToken
user authorization URLhttps://www.google.com/accounts/OAuthAuthorizeToken
access token URLhttps://www.google.com/accounts/OAuthGetAccessToken

consumer.propertiesを読み込む

// クラスローダーを生成
ClassLoader loader = ClassLoader.getSystemClassLoader();
// 設定ファイルのURLを取得
URL url = loader.getResource("consumer.properties");
// 読み込み
Properties prop = ConsumerProperties.getProperties(url);

// tomokeyという名前の付いた設定を読み込む
ConsumerProperties consumers = new ConsumerProperties(prop);
OAuthConsumer consumer = consumers.getConsumer("tomokey");

リクエストトークンを取得する

// OAuthClient
OAuthClient client = new OAuthClient(new HttpClient4());

// OAuth accessor
OAuthAccessor accessor = new OAuthAccessor(consumer);

// パラメータを構築
List<Parameter> params = new ArrayList<Parameter>();
// oauth_callbackを追加
params.add(new Parameter(
    OAuth.OAUTH_CALLBACK,
    accessor.consumer.callbackURL));

// リクエストトークン取得を要求
client.getRequestToken(accessor, "GET", params);

// 取得したリクエストトークン
String token = accessor.requestToken;
String secret = accessor.tokenSecret;
ライブラリを使っているおかげでだいぶ簡潔になっているけど、本当は色々とやってくれている。具体的には、下記のことをやっている。
base stringの構築
これらの文字列を全部連結する。
  • HTTPリクエストメソッド(POSTとかGET)
  • アクセスURL
  • 全リクエストパラメータを「key=value」形式にしてkeyでソートして「&」でくっつける…
oauth_signatureの構築
デフォルトのSHA-1アルゴリズムでbase stringに対するハッシュ値を計算し、base64エンコードをかけて生成する。
SHA-1の鍵にはconsumer secretの末尾に「&」を付与したものを用いる。
HTTPリクエストの構築と実行
oauth_signatureを含む全てのパラメータをBODY部に設定し、Content-Type:application/x-www-form-urlencodedとしてリクエストを発行する。
APIに依っては、パラメータをHEADER部に行わなければならない場合もあるので、consumer.propertiesに「tomokey.consumer.parameterStyle: AUTHORIZATION_HEADER」とか設定すると吉。


ユーザーにアプリケーションを承認してもらう

これはアプリケーションの形態に依存すると思う(Androidアプリならhttp/httpsのインテントを投げるとか)けど、いずれにしても下記のように作成したURLに対して、ブラウザでアクセスしてもらう必要がある。

// サービスプロバイダーが定義しているエンドポイント
String authUrl
  = accessor.consumer.serviceProvider.userAuthorizationURL;

// ここで作成したURLにアクセスしてもらう
String url
  = OAuth.addParameters(authUrl, OAuth.OAUTH_TOKEN, token);
大抵の場合は承認ボタンがあるのでポチっとしてもらう。その後、Webアプリケーション(callbackURLが設定されている)の場合は、そのURLへのアクセス時にパラメータとしてverifierが送信される。(リダイレクトされる)

クライアントアプリケーション(callbackURLがoob)の場合は、画面にverifierが表示される。(ユーザーにコピペしてもらう必要がある)


アクセストークンを取得する

// OAuthClient
OAuthClient client = new OAuthClient(new HttpClient4());

// OAuth accessor
OAuthAccessor accessor = new OAuthAccessor(consumer);

// SHA1の鍵にリクエストトークンのシークレットのほうが必要
accessor.tokenSecret = secret;

// パラメータを構築
List<Parameter> params = new ArrayList<Parameter>();
// oauth_tokenとしてリクエストトークンを追加
params.add(new Parameter(
    OAuth.OAUTH_TOKEN,
    token));
// oauth_verifierを追加
params.add(new Parameter(
    OAuth.OAUTH_VERIFIER,
    "ベリファイア"));

// アクセストークン取得を要求
client.getAccessToken(accessor, "GET", params);

// 取得したアクセストークン
String accToken = accessor.accessToken;
String accSecret = accessor.tokenSecret;
リクエストトークンの取得と似ているけど、注意点がいくつかある。
  • accessor.tokenSecretにリクエストトークンのシークレットのほうを設定する。(SHA1ハッシュ計算時に使う鍵として利用)
  • リダイレクトかコピペかで入力してもらったベリファイアをパラメータとして設定する。

終わり

一応、アクセストークンを使ってAPIアクセスするコードものっけておく。

// URL(Fusion Tables API)
String url = "https://www.google.com/fusiontables/api/query";

// パラメータ
List<Parameter> params
  = OAuth.newList("sql", "SELECT * FROM 913439");

// OAuthClient
OAuthClient client = new OAuthClient(new HttpClient4());

// OAuth accessor
OAuthAccessor accessor = new OAuthAccessor(consumer);

// アクセストークンを設定
accessor.accessToken = accToken;
accessor.tokenSecret = accSecret;

// リクエストオブジェクト生成
OAuthMessage request
  = accessor.newRequestMessage("POST", url, aParams);

// 実行
OAuthMessage response
  = client.access(request, ParameterStyle.AUTHORIZATION_HEADER);
但し、リダイレクトの考慮をしていないので注意。

ライブラリにして公開した

すぐ上に書いてあるリダイレクトの考慮をした上で、ちょっと便利っぽくライブラリにまとめてみた。 ここ(Google Code)からダウンロード可能。

OAuthとは何か?

OAuth(1.0)については、ゼロから学ぶOAuthとかAPIアクセス権を委譲するプロトコル、OAuthを知るあたりで色々と説明されているけど、忘れた時にふと読み返すには少し記事が長いと思う。

今回は、ざっくりとOAuthとは何か、その承認プロセスはどんな感じかを自分なりに短くまとめてみる。


何をするためのものか

例えば、自分のGoogleカレンダーを見やすく表示してくれるアプリケーションがあったとする。これを利用するには、アプリケーションの入手の他に、Googleカレンダーデータの取得が必要になる。

Googleカレンダーデータの取得を実現する一番簡単な方法は、アプリケーションにGoogleのIDとパスワードを設定すること。でも、IDとパスワードをアプリケーションに設定するのがちょっとイヤな場合がある。Webアプリケーションなどがそれ。

そんな時、OAuthを使うとGoogleのIDとパスワードをアプリケーションに教えることなく、やりたいことが実現できる。


OAuthのプレイヤー

OAuthでは、下記3つのプレイヤーが鍵になってくる。

サービスプロバイダー

GoogleやTwitterなどに代表されるサービスプロバイダーは、OAuthにおいて最も重要な役割を担っている。エンドポイントの提供、各種トークンやベリファイアの発行を行う。

コンシューマ
サービスプロバイダーが提供するAPIを利用してユーザーのデータを扱うアプリケーション。

ユーザー
アプリケーション利用者であり、サービスプロバイダーに会員登録をしている。


OAuthで使用する要素

OAuthを実装するにあたって基本的な要素を理解する必要性がある。

トークン(token/token secret)

トークンとトークンシークレットで1組の鍵データであり、コンシューマトークン、リクエストトークン、アクセストークンの3種類が存在する。

コンシューマトークン(consumer token/consumer token secret)
サービスプロバイダーがアプリケーションに対して発行するもの。アプリケーションを識別するために利用する。

リクエストトークン(request token/request token secret)
リサービスプロバイダーがアプリケーションに対して一時的に発行するもの。ユーザーがアプリケーションを承認するプロセスでのみ利用する。

アクセストークン(access token/access token secret)
サービスプロバイダーがアプリケーションに対して発行するもの。ユーザーデータにアクセスし放題になる重要情報。アクセストークンさえあればOAuthを利用した各種APIの利用が可能になる。

ベリファイア(verifier)
ユーザーがアプリケーションを承認するプロセスの最終段階で利用するもの。PINコードと呼ばれたりする。サービスプロバイダーがユーザーに対して発行するものであり、さらにユーザーがアプリケーションに渡し、最後にアプリケーションがリクエストトークンとセットでサービスプロバイダーに投げつける。

エンドポイント(Endpoint)
サービスプロバイダーが公開しているOAuth認証プロセスに必要な機能を備えたURI。リクエストトークンを発行するためのエンドポイント、ユーザーが承認を行うためのエンドポイント、アクセストークンを発行するためのエンドポイントが存在する。

OAuth 1.0 API Reference(Google)とかTwitter REST API Method: oauth request_token(Twitter)に書いてある。


承認プロセス

まず、アプリケーションの登録

アプリケーションの登録方法はサービスプロバイダーによって違うけど、基本的には専用のWebサイトでアプリ登録を行い、コンシューマトークンを得るという作業になる。

Googleの場合

ここにアクセスし、画面の指示に従う。が、完全なクライアントアプリケーションを作りたいよーという場合には、すっとばしてOK。コンシューマトークンには"anonymous"という文字列を利用すれば良い。

Twitterの場合
ここにアクセスし、画面の指示に従う。Twitterの場合はWebアプリケーションでもクライアントアプリケーションでも同じ。



リクエストトークンの取得

リクエストトークンを発行するためのエンドポイントに対して、必要なパラメータを設定したHTTPリクエストを発行し、レスポンスとしてリクエストトークンを得る。



ベリファイアの取得

ユーザーが承認を行うためのエンドポイントへユーザーにアクセスしてもらう。その際、oauth_tokenパラメータとして先に取得したリクエストトークンを付与する。ユーザーが承認を行うとベリファイアを得ることができる(サービスプロバイダのサイト上に表示されるか、指定したURLへのリダイレクトかのいずれか)。



アクセストークンの取得

アクセストークンを発行するためのエンドポイントに対して、必要なパラメータを設定したHTTPリクエストを発行し、レスポンスとしてアクセストークンを得る。



終わり

この長い旅路を終えて得たアクセストークンが、OAuthを認証方式に採用しているAPIを利用するために必要なものになる。

2011年5月22日日曜日

Androidの基本 ListViewを使う

今回は、基本に戻ってListViewを使った画面の作成について書いてみる。がしかし、それだけでは面白くないので一切の画像ファイルを使わずに実現してみる。出来上がりのイメージはこんな感じ。


やること

  • 画面のレイアウト定義
  • 1行分のレイアウト定義
  • リストアダプタの作成
  • アクティビティクラスの作成

画面のレイアウト定義

画面のレイアウトはシンプルに、ListViewのみを配置した形に定義する。同じことを実現するためにListActivityというのが用意されているが、使用するメリットが薄い上にこれ系のクラス(TabActivityとか)には問題があったりもするので使わない。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
    <ListView
      android:id="@+id/listview"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"/>
</LinearLayout>

1行分のレイアウト定義

ListViewは、各行のレイアウトを自由にカスタマイズすることができる。単純に文字だけを表示するならAndroid標準で用意されているレイアウトを使うことも可能。

今回は、右端に三角形のマークを表示したいので、下記のようにLinearLayoutを定義、子要素としてTextViewとカスタムビューを配置する。TextViewにはlayout_weight="1"のオマジナイがしてあるのでTextViewの領域は限界まで広がり、残りの部分に三角形マークが配置される。

<?xml version="1.0" encoding="UTF-8"?>  
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"  
  xmlns:tomokey="http://schemas.android.com/apk/res/com.tomokey"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:orientation="horizontal"
    android:gravity="center_vertical" >
    
    <TextView 
      android:id="@+id/row_text" 
      android:layout_height="wrap_content"
      android:layout_width="wrap_content"  
      android:layout_weight="1"
      android:textColor="@android:color/white"
      android:textSize="20dip"
      android:minHeight="50dip"
      android:gravity="center_vertical"/>
  
    <com.tomokey.custom.view.Triangle
      android:id="@+id/tri"
      android:layout_width="7dip"
      android:layout_height="15dip"
      tomokey:color="@android:color/white"
      tomokey:direction="right"
      />
    
</LinearLayout>
※画像を使わない縛りがあるために、com.tomokey.custom.view.Triangleというカスタムビューを利用している。俺は画像を使っちゃうぜ!って場合はImageViewとかにしておけばOK。

リストアダプタの作成

リストアダプタ(ListAdapter)はその名の通り、データとListViewを繋ぐためのもの。今回は配列データとListViewを繋ぐためのArrayAdapterを使って下記のようにリストアダプタを作成する。

ArrayAdapterのコンストラクタには、1行分のレイアウト定義とTextViewを指し示すIDを指定する。ArrayAdapterをそのまま利用する場合には、1行分のレイアウト定義にTextViewが含まれていなければならないし、そのTextViewにはIDが設定されていなければならない。

ListAdapter adapter
  = new ArrayAdapter<String>(
    this,
    R.layout.row,
    R.id.row_text,
    DATA)

上記のコードで利用しているDATAはこんな感じ。

private static final List<String> DATA
  = Arrays.asList(new String[]{
          "Google",
          "Apple",
          "Microsoft",
          "Research In Motion",
          "Palm"
    });

アクティビティクラスの作成

お決まりのコード達に加えて、ListViewウィジェットへリストアダプタを設定するコード(setAdapter)を追加する。

@Override
public void onCreate(Bundle savedInstanceState)
{
  super.onCreate(savedInstanceState);

  // 画面のレイアウト定義を設定
  setContentView(R.layout.main);
  
  // ListViewウィジェットを取得
  mListView = (ListView) findViewById(R.id.listview);

  // この辺りで「リストアダプタの作成」をしておく

  // リストアダプタを設定
  mListView.setAdapter(adapter);
}

三角形マークは画像で行くぜ!って場合はここまでで終わり。ここからは、三角形をプログラムのみで実現する方法。

三角形のためにやること

  • 三角形のプロパティ設計
  • カスタムビュークラスの作成

三角形のプロパティ設計

設計などと仰々しく書いてみたけど、単純に三角形を使うとき何が変えられたら便利かを考えてXMLを書くだけ。今回は以下のように定義してみた。

変えられたら便利なとこ

三角形の色 三角形の方向

XML - res/values/attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="Triangle">
    <attr name="color" format="color" />
    <attr name="direction">
      <enum name="right" value="0"/>
      <enum name="left" value="1"/>
      <enum name="top" value="2"/>
      <enum name="bottom" value="3"/>
  </attr>
  </declare-styleable>
</resources>

Triangleという名前のカスタムビューについて、colorとdirectionという名前のプロパティを定義している。また、directionについては「right」「left」「top」「bottom」のいずれかが設定できて、それぞれ数字の0~3の値を持っている。

カスタムビュークラスの作成

カスタムビューとは、Androidアプリの各ウィジェットの基礎になっているViewクラスを独自に拡張して作成した、世界でたった一つの私だけのビュー・・・だ。以下、簡潔に。

定義
package com.tomokey.custom.view;

public class Triangle extends View
定数
// 三角形の方向
public final static int DIRECTION_RIGHT = 0;
public final static int DIRECTION_LEFT = 1;
public final static int DIRECTION_TOP= 2;
public final static int DIRECTION_BOTTOM = 3;
インスタンス変数
// 描画オブジェクト
private Paint mPaint;

// 三角形の色
private int mColor;

// 三角形の方向
private int mDirection;
コンストラクタ
public Triangle(Context context, AttributeSet attrs)
{
  super(context, attrs);

  // 描画オブジェクトを初期化
  mPaint = new Paint();
  mPaint.setAntiAlias(true);
  
  // XMLで定義されたプロパティ値を取得
  TypedArray a =
    context.obtainStyledAttributes(
        attrs,
        R.styleable.Triangle);
  
  // 三角形の色を取得(デフォ:白)
  mColor 
    = a.getColor(
        R.styleable.Triangle_color,
        0xFFFFFFFF);
  
  // 三角形の方向を取得(デフォ:右)
  mDirection
    = a.getInt(
        R.styleable.Triangle_direction,
        DIRECTION_RIGHT);
}
プログラムから色を設定するメソッド
public void setColor(String color)
{
  mColor = Color.parseColor(color);
  requestLayout();
  invalidate();
}
ビューの描画メソッド
@Override
protected void onDraw(Canvas canvas)
{
  super.onDraw(canvas);
  
  // 三角形の色を設定
  mPaint.setColor(mColor);
  
  // 三角形の方向に応じてパスを生成
  Path triangle = new Path();
  switch (mDirection)
  {
  case DIRECTION_RIGHT:
    // 左上から
    triangle.moveTo(0, 0);
    // まっすぐ左下へ
    triangle.lineTo(0, getMeasuredHeight());
    // んで、右端の高さは真ん中へ
    triangle.lineTo(getMeasuredWidth(), (getMeasuredHeight() / 2));
    break;
    
  case DIRECTION_LEFT:
    triangle.moveTo(getMeasuredWidth(), 0);
    triangle.lineTo(getMeasuredWidth(), getMeasuredHeight());
    triangle.lineTo(0, (getMeasuredHeight() / 2));
    break;
    
  case DIRECTION_TOP:
    triangle.moveTo(0, getMeasuredHeight());
    triangle.lineTo(getMeasuredWidth(), getMeasuredHeight());
    triangle.lineTo((getMeasuredWidth() / 2), 0);
    break;
    
  case DIRECTION_BOTTOM:
    triangle.moveTo(0, 0);
    triangle.lineTo(getMeasuredWidth(), 0);
    triangle.lineTo((getMeasuredWidth() / 2), getMeasuredHeight());
    break;
  }
  
  // パスを塗りっと
  canvas.drawPath(triangle, mPaint);
}