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);
}