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はこれのこと。