2013年12月21日土曜日

POJOをActivityにしちゃうDIコンテナ「Transfuse」の話。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 こんにちは。Androidライブラリおじさんです。Android Advent Calendar 2013 21日目の担当です。TransfuseというAndroid向けの凶悪なDIコンテナがあったので人柱がてら触ってみました。使い方や出来る事などを簡単にまとめます。



サンプルコードはhttps://github.com/sys1yagi/TransfuseSampleで公開してます。Android Studio用です。

宣伝


 本出たり出そうだったりしてます。どっちもいい感じです。年末年始に是非!

Androidオープンソースライブラリ徹底活用 Effective Android
aa bb


結論


 TransfuseはPOJOにアノテーションベースでDIするので設計がだいぶ縛られるけど、これこそDIの正しい設計なのかもしれないと思いました。Transfuseすごい。こわい。ただアノテーションと普通のクラス名が被るのがめんどい。 @Activityとか@Fragmentとか@Viewとか。どっちかをFQDNにしないといけない。だるい。あと他のライブラリとのインテグレーションが出来るのかよくわからない。多分問題なく出来ると思うが・・・。ボイラープレートなコードは大分減るので楽。

環境構築


 Transfuseは元々はmaven用の様です。Android Studioで使う場合はいくつか手順が必要です。Android Studioで新規プロジェクトを作成した状態とします。

build.gradleの設定


 まずbuild.gradleを色々いじります。Transfuseに必要な依存性を管理するために、configurationsブロックにtransfuseを追加します。

configurations {
    compile
    transfuse.extendsFrom(compile)
}

 次にdependenciesを追加します。

dependencies {
    compile 'com.android.support:appcompat-v7:+'
    transfuse 'org.androidtransfuse:transfuse:0.2.2'
    transfuse 'com.google.android:android:2.1_r1'
    transfuse 'com.google.android:support-v4:r7'
    transfuse 'org.parceler:parceler:0.1.1'
    compile 'org.androidtransfuse:transfuse-api:0.2.2'
    compile 'org.parceler:parceler-api:0.1.1'
}

 androidブロックにsourceSets.mainブロックを追加してAndroidManifest.xmlの場所を指定します。

android {
    sourceSets.main {
        manifest.srcFile 'AndroidManifest.xml'
    }
}

 build.gradleの最下部辺りに全variantでtransfuseを実行する設定を書きます。

android.applicationVariants.all { variant ->

  def aptOutput = file("${project.buildDir}/source/r/${variant.dirName}")
  aptOutput.mkdirs()
  aptOutput.eachFileRecurse groovy.io.FileType.FILES, {
    if (it.name.equals('R.java')) {
      return
    }
    it.delete()
  }
  variant.javaCompile.options.compilerArgs += [
      '-processorpath', configurations.transfuse.asPath,
      '-s', aptOutput
  ]

}

 全体はコチラで参照できます。→TransfuseSample/TransfuseSample/build.gradle

AndroidManifest.xmlを移動する


 Transfuseのタスクを実行する際にAndroidManifest.xmlを読みにいくのですが、以下のようにデフォルトの場所に置いているとうまく読んでくれません。

$MODULE_NAME/src/main/AndroidManifest.xml

 以下のパスに移動して下さい。

$MODULE_NAME/src/AndroidManifest.xml

 これで利用可能になるはずです。

基本的な使い方


 TransfuseはPOJOに対して必要なアノテーションを付加していくスタイルになります。例えば簡単なActivityだと以下の様になります。

@Activity(label = "@string/app_name")
@Layout(R.layout.activity_main)
@IntentFilter({
  @Intent(type = IntentType.ACTION, name = android.content.Intent.ACTION_MAIN),
  @Intent(type = IntentType.CATEGORY, name = android.content.Intent.CATEGORY_LAUNCHER)
})
public class Main {
    @OnCreate
    public void create(Bundle saved) {
      // initialize
    }
}

 このコードをコンパイルすると、MainActivity.javaやインジェクタなどが生成されます。また、AndroidManifest.xmlを自動生成してくれます。@Activityや@IntentFilterなどAndroidManifest.xmlに関連するアノテーションを付加しておくとAndroidManifest.xmlに反映されます。

 最初AndroidManifest.xmlを自動生成する機能が動きませんでした。色々いじっていたら出来るようになったんですが条件がいまいちわかりません。自動生成が動かない場合は手でAndroidManifest.xmlにActivity等の定義を追加しましょう。

<?xml version="1.0" encoding="utf-8"?>
<manifest
  xmlns:android="http://schemas.android.com/apk/res/android" 
  package="jp.mydns.sys1yagi.android.transfuse" 
  android:versionCode="1" android:versionName="1.0">

    <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19"/>

    <application android:name="android.app.Application">
        <activity android:label="@string/app_name" android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

 あとはビルド&実行。

./gradlew --daemon installDebug
adb shell am start -n jp.mydns.sys1yagi.android.transfuse/.MainActivity --activity-clear-top

Transfuseの機能


 Transfuseは沢山のアノテーションや機能を提供しています。ざっくり各種機能を説明します。

Activity Lifecycle Method


 POJOでActivityを実現するので、ライフサイクルメソッドはすべてアノテーションで指定します。

@Activity
@Layout(R.layout.activity_lifecycle_methods)
public class ActivityLifecycleMethods {
  @OnCreate
  public void create() {

  }
  @OnResume
  public void resume() {

  }

  @OnPause
  public void pause() {

  }
}

 大体以下のアノテーションが使えます。@OnNewIntentが無いのが気になります。

@OnCreate
@OnStart
@OnPause
@OnResume
@OnStop
@OnDestroy
@OnBackPressed
@OnSaveInstanceState
@OnRestoreInstanceState
@OnConfigurationChanged

View Injection


 Viewのインジェクションは@Viewを使います。RoboGuiceとかだと@InjectViewですが、Transfuseだと@Injectと@Viewの組み合わせになります。

@Activity
@Layout(R.layout.fragment_view_injection)
public class ViewInjection {

  @Inject
  @View(R.id.text)
  TextView mTextView;

  @Inject
  @View(tag="hello_button")
  Button mButton;

  @OnCreate
  private void init(){
    mTextView.setText("Hello Transfuse!!");
    mButton.setText("Tag Injection!!");
  }
}

 面白いのはViewのID指定だけでなく、tagでの指定もできる点でしょうか。上記コードで利用しているレイアウトは以下の通りです。

<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">

  <TextView
    android:id="@+id/text"
    android:text="@string/hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

  <Button
    android:id="@+id/button"
    android:tag="hello_button"
    android:text="@string/hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</LinearLayout>

Extra Injection


 Activityで受け取るIntentのパラメータもインジェクションできます。@Extraでキー名を指定できるほか、パラメータがオプショナルかどうかも指定できます。

@Activity
public class ExtraInjection {

  @Inject
  @Extra("title")
  String mTitle;

  @Inject
  @Extra(value = "message")
  String mMessage;

  @Inject
  @Extra(value = "count", optional = false)
  Integer mCount;

}

 上記コードをコンパイルすると、「ExtraInjectionActivityStrategy.java」が生成されます。このクラスは対象のActivityを起動するIntentを生成するクラスです。ExtraInjectionActivityStrategyはコンストラクタで必須パラメータを受け取り、オプショナルなパラメータについてはsetterメソッドが生やされます。

@Inject
IntentFactory mIntentFactory;

private void start(){
  mIntentFactory.start(
    new ExtraInjectionActivityStrategy("Transfuse!", "hello")
      .setCount(10)
  );
}

 Transfuseが提供するIntentFactoryクラスのstartメソッドにインスタンスを渡すことでActivityを起動できます。

Resource Injection


 リソースのインジェクションは @Resourceを使います。対応する型が間違っているとコンパイル時にエラーになるか、運良く通っても実行時にクラッシュします。

@Activity
public class ResourceInjection {

  @Inject
  @Resource(R.string.injection_text)
  String mInjectionText;

  @Inject
  @Resource(R.drawable.ic_launcher)
  Drawable mIcLauncher;

}

Preference Injection


 @Preferenceを使ってSharedPreferenceの値をフィールドにインジェクションできます。

@Fragment
public class PreferenceInjection {
    @Inject
    @Preference(value = "name")
    String mName;

    @Inject
    @Preference(value = "is_notification", defaultValue = "false")
    Boolean mIsNotification;
}

 @Preferenceを持つクラスをビルドすると以下の様なコードで値が解決されます。任意に名前を付けたSharedPreferenceを利用する事はできないようです。

PreferenceManager.getDefaultSharedPreferences(this).getString("name")

SystemService Injection


 SystemServiceは@SystemServiceを使います。アノテーションにSystemServiceの名前をセットすれば対応したクラスがインジェクションされます。

@Activity
public class SystemServiceInjection {

  @Inject
  @SystemService(Context.LAYOUT_INFLATER_SERVICE)
  LayoutInflater mLayoutInflater;

}

Application Lifecycle Method


 ApplicationクラスもPOJOで書けちゃいます。

@Application(name = "TransfuseApplication")
public class TransfuseApplicationBase {

  @OnCreate
  public void create() {

  }

}

 以下のアノテーションが使えます。

@OnCreate
@OnTerminate
@OnLowMemory
@OnConfigurationChanged

Fragment Lifecycle Method


 FragmentももちろんPOJOです。Activityと同じ様に、@Fragmentで作成できます。

@Fragment
@Layout(R.layout.fragment_fragment_lifecycle_method)
public class FragmentLifecycleMethod {

  @OnActivityCreated
  void activityCreated() {

  }

  @OnDestroyView
  void destroyView(){

  }
}

 以下のLifecycle Methodのアノテーションが使えます。@OnCreateは何故かonCreateView()にバインドされます。onAttachやonDetachのアノテーションもありません。まぁあんまり使わないしいいんじゃないでしょうか。

@OnCreate
@OnActivityCreated
@OnStart
@OnResume
@OnPause
@OnStop
@OnDestroyView
@OnDestroy
@OnLowMemory
@OnConfigurationChanged

 @Fragmentにはnameとtypeの属性をセットできます。name属性は生成するFragmentの名前を指定できます。以下の例だとLifecycleMethodFragment.javaが生成されます。デフォルトではクラス名のSuffixに"Fragment"が付きます。

@Fragment(name="LifecycleMethodFragment")
public class FragmentLifecycleMethod {

}

 type属性を設定するとextendsするクラスを指定できます。但しClass<? extends android.support.v4.app.Fragment>である必要があります。

import android.support.v4.app.ListFragment;

@Fragment(type = ListFragment.class)
public class FragmentList{

}

 Fragmentを使う場合、Activity側はFragmentActivityを利用する必要があります。Activity側でもtype属性でFragmentActivityを指定しておきましょう。

@Activity(type = FragmentActivity.class)
public class FragmentContainer {

}

 <fragment>でFragmentを埋める分には特に問題出ないですが、動的にFragmentを切り替える場合ちょっと面倒です。サンプルではこうしてますがもうちょいいい感じに出来るんじゃないかと思っています。

BroadcastReceiver Lifecycle Method


 BroadcastReceiverもPOJO(ry。@BroadcastReceiverと@Intentで定義できます。

@BroadcastReceiver
@Intent(type = IntentType.ACTION, name = Action.ACTION)
public class Action {
  public final static String ACTION = "jp.mydns.sys1yagi.android.transfuse.ACTION";

  @OnReceive
  public void boot(Context context, android.content.Intent intent) {
    //do something
  }
}

 投げる方はこんな感じ。

@Fragment
@Layout(R.layout.fragment_broadcast)
public class Broadcast {

  @Inject
  Context mContext;

  @RegisterListener(R.id.button)
  View.OnClickListener onClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
      Intent intent = new Intent(Action.ACTION);
      mContext.sendBroadcast(intent);
    }
  };
}

 ActivityやFragmentとなるクラスはPOJOなのでsendBroadcast()とか持ってません。なのでフィールドにContextを@Injectしておいて、それを使います。以下の場合だとmContextにはgetActivity()の値がインジェクションされます。

Service Lifecycle Method


 ServiceもPOJO(ry。ただOnBindが無かったりでまだ未完成感が・・・。こういうIssueもありますが進んでない感。一応@Serviceで定義できます。でも@OnCreateと@OnDestroyしかサポートしてません。

@Service
@IntentFilter(@Intent(type= IntentType.ACTION, name="transfuse"))
public class Calc{
  @OnCreate
  public void create(){
    Log.d(TAG, "onCreate");
  }

  @OnDestroy
  public void destroy(){
    Log.d(TAG, "onDestroy");
  }

}

Listener Registration


 BroadcastReceiver Lifecycle Methodの所のコードに出てますが、@RegisterListenerでリスナをViewにバインドできます。

@RegisterListener(R.id.button)
View.OnClickListener mListener = new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    mTextView.setText("Pushed!!!!!!!");
  }
};

@RegisterListener(value = R.id.long_click_text)
View.OnLongClickListener mLongClickListener = new View.OnLongClickListener() {
  @Override
  public boolean onLongClick(View v) {
    mTextView.setText("Long Pushed!!!!!!!");
    return false;
  }
};

 以下のViewクラスの持つリスナをサポートしています。API Level 11以降に追加されたものはサポートしてません。

View.OnLongClickListener
View.OnClickListener
View.OnCreateContextMenuListener
View.OnFocusChangeListener
View.OnKeyListener
View.OnTouchListener

Call-Through Events


 Call-Through EventsはPOJO側で定義したメソッドを生成コード側に生やす機能です。イベント用のインタフェースがあるので、それをPOJO側で実装します。例えばActivityのonKeyDownを実装するには、POJO側でActivityOnKeyDownListenerインタフェースを実装しておきます。

@Activity
@Layout(R.layout.fragment_call_through_events)
@RegisterListener
public class CallThroughEvents implements ActivityOnKeyDownListener {

  public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
    return false;
  }

}

 上記コードをコンパイルすると、以下のコードが生成されます。CallThroughEventsActivityされたCallThroughEventsActivity側にonKeyDownメソッドが生え、CallThroughEventsのonKeyDown()を呼び出している事がわかります。

public class CallThroughEventsActivity
  extends Activity
  implements ContextScopeHolder
{

  private jp.mydns.sys1yagi.android.transfuse.CallThroughEvents callThroughEvents$0;

//略

  @Override
  public boolean onKeyDown(int int$1, KeyEvent keyEvent$0) {
    return callThroughEvents$0 .onKeyDown(int$1, keyEvent$0);
  }
//略
}

 以下のCall-Through用のインタフェースが用意されています。Service用のもありましたがうまく動きませんでした。

ActivityMenuComponent
ActivityOnKeyDownListener
ActivityOnKeyLongPressListener
ActivityOnKeyMultipleListener
ActivityOnKeyUpListener
ActivityOnTouchEventListener
ActivityOnTrackballEventListener

Method Intercepters


 @Asynchronous、@UIThreadをメソッドに付加する事で、メソッドを実行するスレッドを設定できます。以下の様に通信処理などをするメソッドに@Asynchronousを付けておき、結果を反映するメソッドに@UIThreadを付けておけば、非同期処理とUIスレッドの連携を簡単に書けます。

@Fragment
public class MethodInterceptors {

  @Asynchronous
  public void loadAsynchronous() {
    try {
      URL url = new URL(LOAD_URL);
      String result = IOUtils.toString(url);
      setResult(result);
    } catch (Exception e) {
      e.printStackTrace();
      setResult(e.getMessage());
    }
  }

  @UIThread
  public void setResult(String text) {
    mTextView.setText(text);
  }

}

Events Bus


 TransfuseはEvent Busの機能も提供しています。以下の様に任意のイベントクラスを定義しておきます。

public class MessageEvent {

  private String mMessage;

  public MessageEvent(String message) {
    mMessage = message;
  }

  public String getMessage() {
    return mMessage;
  }
}

 イベントの送信はEventManagerを使います。以下の様に@InjectでEventManagerのインスタンスを持っておき、任意のタイミングでtriggerメソッドでイベントを送信します。

@Inject
EventManager mEventManager;

@RegisterListener(R.id.button)
View.OnClickListener mListener = new View.OnClickListener() {
  @Override
  public void onClick(android.view.View v) {
    mEventManager.trigger(new MessageEvent("send event!!"));
  }
};

 イベントを受け取りたいクラスで@Observesを付加してメソッドを定義しておきます。EventManagerのtriggerメソッドでイベントを送信すると、@Observesが付加されていて引数がイベントの種類に一致するメソッドが呼び出されます。

@Observes
public void onMessage(MessageEvent event) {
  mTextView.setText("receive event = " + event.getMessage());
}

 この仕組みを使うとリスナなどの登録無しでイベントのやりとりが出来ます。例えばログイン状態が変化した時や、Push通知を受け取った時などにEvent Busを使ってイベントを投げる事で、関連した画面やクラスで処理を行えます。

Parcel


 モデルクラスに@Parcelを付加するとParcelableになります。IntelliJ Plugin for Android Parcelable boilerplate code generationなんかいらんかったんや!

@Parcel
public class SampleData {

  private int mId;

  private String mName;

  private String mDescription;

}

ImplementedBy


 @ImplementedByはインタフェースのデフォルト実装を宣言するアノテーションです。以下のインタフェースがあるとします。

public interface ICalculator {
  public int add(int a, int b);
}

 これを実装したCalculatorImplクラスがあるとします。

public class CalculatorImpl implements ICalculator {
  @Override
  public int add(int a, int b) {
    return a + b;
  }
}

 以下の様にICalculatorをインジェクションしたいクラスがあった時、Transfuse側にこの依存性を解決する為の情報を教えてあげる必要があります。依存性を解決する為の情報を教える方法の1つが@ImplementedByです。

@Fragment
public class ImplementedBy {

  @Inject
  ICalculator mCalculator;

}

 インタフェース側で以下の様に@ImplementedByでデフォルトの実装クラスを定義しておくと、@Inject ICalculatorに指定したクラスがインジェクションされます。

@ImplementedBy(CalculatorImpl.class)
public interface ICalculator {
  public int add(int a, int b);
}

Module


 @TransfuseModuleで依存性を解決するモジュールを定義できます。モジュールに@Injectを解決する際に利用する情報を定義していきます。

@TransfuseModule
public class ApplicationModules {

}

@Provider


 モジュール内で@Providesを付加したメソッドを定義しておくと、@Injectを解決する際に利用されます。以下の例だとArrayAdapterを解決できます。

@TransfuseModule
public class ApplicationModules {
 @Provides
  public ArrayAdapter getAdapter(Context context) {
      return new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1);
  }
}

 ArrayAdapterなど汎用的なクラスの場合、@Inject ArrayAdapterと書いてある全ての部分に上記のプロバイダが適用されます。@Namedを使うと特定のプロバイダを指定できます。

@TransfuseModule
public class ApplicationModules {
  @Provides
 @Named("TopMenuList")
  public ArrayAdapter getAdapter(Context context) {
    return new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1);
  }
  @Provides
 @Named("SubMenuList")
  public ArrayAdapter getSubMenuAdapter(Context context) {
    return new ArrayAdapter<String>(context, R.layout.sub_menu);
  }
}

 以下の様に@Inject側でインジェクションするプロバイダの名前を指定します。

@Inject
@Named("TopMenuList")
ArrayAdapter mAdapter;

Provider


 プロバイダはProviderインタフェースを実装すればモジュールの外部に定義できます。

public class SampleDataProvider implements Provider<SampleData>{
  @Override
  public SampleData get() {
    SampleData sampleData = new SampleData();
    sampleData.setId(-1);
    sampleData.setName("provided data");
    sampleData.setDescription("provided data");
    return sampleData;
  }
}

 定義したプロバイダはモジュール側の@BindProvidersで設定できます。

@TransfuseModule 
@BindProviders({
  @BindProvider(type = SampleData.class, provider = SampleDataProvider.class)
})
public class ApplicationModules {
}

Bind


 直接モジュールに解決するクラスを定義する場合は@Bindingsを使います。@ImplementedByに似ていますが、@ImplementedByはインタフェース側に書くのに対し、@Bindingsはモジュール側に書きます。モジュール側でデフォルトの実装を任意に切り替えられるわけです。

@TransfuseModule
@Bindings({
  @Bind(type = ICalculator.class, to = CalculatorImpl.class)
})
public class ApplicationModules {
}

Factory


 @Injectはしないけど色々解決したい場合は@Factoryを使います。以下の様に@Factoryを付加したクラスを定義します。

@Factory
public interface SampleDataFactory {
  SampleData getSampleData();
}

 以下の様にFactories.get()でFactoryクラスを指定して取り出せます。getSampleData()の戻り値は、コンパイル時にモジュール内の定義に基いて解決されます(@BindProviderや@Bindや@ImplementedByや@Provides)。

public class UseFactory {

  public void getData() {
    SampleData sampleData = 
      Factories.get(SampleDataFactory.class).getSampleData();
      //do something.
  }

}

Scope


 スコープは依存性を解決する時にたどるオブジェクトマップの範囲を定義できます。例えば@Singletonをクラス宣言に付けておくと、そのクラスはシングルトンになります。

@Singleton
public class SingletonCalculator {
  public int multi(int a, int b) {
    return a * b;
  }
}

 例えば以下の様に二つのフィールドに@Injectを書くと、通常は二つのインスタンスがそれぞれのフィールドにインジェクションされます。@Singletonが付いている場合、両方同じインスタンスがインジェクションされます。

@Fragment
public class Scope {
  @Inject
  SingletonCalculator mSingletonCalculator1;

  @Inject
  SingletonCalculator mSingletonCalculator2;
}

 デフォルトでは@Singletonの他にも@ContextScopeがあります。@ContextScopeはActivity単位でインスタンスが分かれます。この他カスタムスコープを自分で定義したり、@Injectをどのスコープで解決するかを指定できる@ScopeReferenceなどがあります。

おわりに


 Transfuseすごい。一個これでアプリ作ってみたい。テスト周りには手が回らなかったのでテストも書きたい。本家ではRobolectricでやっているみたい。

2013年12月12日木曜日

ViewStickerを作りました。

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 ViewStickerというセクションヘッダ固定のような事をScrollView上で出来るライブラリを作りました。ついでにGithubで公開しました。-> コチラ

大体以下の様な動きをします。



potatotips (iOS/Android開発Tips共有会) 第2回


で発表してきました。potatotips2とは





使い方


まずはレイアウトXMLを作ります。ViewStickerを使うにはFragmeLayoutとScrollViewが必要です。以下の様な感じでScrolViewをFrameLayoutでくるんで、ScrollViewに具を詰めます。

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <ScrollView
      android:id="@+id/scroll_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >
      <TextView
          android:id="@+id/description_header"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          />
    </LinearLayout>
  </ScrollView>
</FrameLayout>

次にActivity( or Fragment )でレイアウトを初期化し、ViewStickerを作成します。ViewSticker#starch()でレイアウトを持つActivity( or Fragment )、ScrollView、FrameLayoutのIDを渡すとインスタンス化できます。

次にstickメソッドでViewを渡すと、そのViewがヘッダ固定されます。複数のViewを登録する事もできます。

public class MainActivity extends ActionBarActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //initialize ViewSticker
    ViewSticker sticker = ViewSticker
      .starch(this, R.id.scroll_view, R.id.container);

    //prepare sticky
    sticker.stick(findViewById(R.id.description_header));
  }

}

おわりに


まだ変な挙動があったり、未実装の機能があったりするし、Android Studio用だし、Maven Centralには登録してないしで色々やる事ありますが、ちょこちょこやろうかと思います。Pull Request大歓迎です!

2013年11月17日日曜日

Android 4.4 詳解 Printing Framework 後編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 1週間ってアッという間ですね。「Android 4.4 KitKat 冬コミ原稿リレーを開催」の11/9担当のsys1yagiです。

 Printing Frameworkの後編という事で、PrintServiceについて解説します。印刷の出力部分を実装する話は「Android 4.4 詳解 Printing Framework 前編」を参照して下さい。

 サンプルコードとしてDropboxにPDFをアップロードするDropboxPrintServiceを作りました。ソースはコチラ→DropboxPrintService

PrintServiceとは


 PrintServiceはプリンタの検出と、印刷ジョブの処理を行うサービスです。ネットワークプリンタのドライバの様な役割をする他、PDFを任意の場所に出力するサービスとしても実装できます。

PrintServiceの追加や設定


 PrintServiceの実装を含んだアプリケーションをインストールすると、SettingsのPrinting画面にサービスが追加されます(図01)。ユーザはそれぞれのPrintServiceのOn/Offを切り替えたり、各PrintServiceが提供する設定画面を起動したりできます。


図01 [PrintServiceの詳細画面]

PrintServiceの実行フロー


 PrintServiceはシステムが管理するPrintManagerと連携しながら動作します。PrintServiceの実行フローを[図02]に示します。



[図02 PrintServiceの実行フロー]

  1. PrintManagerが印刷ダイアログを表示する
  2. システムからプリンタの検出セッションの開始を要求される(onCreatePrinterDiscoverySessionメソッドの呼び出し)
  3. PrinterDiscoverySessionクラスのインスタンスを返却する
  4. PrinterDiscoverySessionクラス内でプリンタの追加、削除、更新を行う
  5. ユーザがプリンタの選択、設定を行い、印刷を開始する
  6. onPrintJobQueuedメソッドにPrintJobが渡される
  7. PrintJobの情報を元に印刷処理を行う
 これらのフローのうち、3,4,5の処理をPrintService側で実装することになります。

PrintServiceを作る


 では早速PrintServiceを実装してみましょう。PrintServiceはServiceを継承した抽象クラスです。[リスト01]に示す通り3つの抽象メソッドを持っています。

リスト01 [PrintServiceの持つ抽象メソッド]

public class SamplePrintService extends PrintService {

  @Override
  protected PrinterDiscoverySession onCreatePrinterDiscoverySession() {
    // プリンタの検索セッション用オブジェクトを返す
    return null;
  }

  @Override
  protected void onPrintJobQueued(PrintJob paramPrintJob) {
    // 印刷処理をする
  }

  @Override
  protected void onRequestCancelPrintJob(PrintJob paramPrintJob) {
    // キャンセルリクエスト
  }

}

onCreatePrinterDiscoverySession()


 システムがプリンタの検出を開始したい時、PrintServiceのonCreatePrinterDiscoverySessionメソッドが呼び出されます。この時PrintService側はPrinterDiscoverySessionのインスタンスを返却する必要があります。

 PrinterDiscoverySessionはプリンタの検出をする間、システムとPrintService間のやりとりをカプセル化するクラスです。PrinterDiscoverySessionクラスの各コールバックメソッドが呼び出されるので、プリンタの追加や更新、削除を行います。

 PrinterDiscoverySessionクラスは沢山の抽象メソッドを持っています。[表01]にPrinterDiscoverySessionクラスの抽象メソッドの解説を示します。

表01 [PrinterDiscoverySessionの抽象メソッド]

項目 解説
void onStartPrinterDiscovery(List<PrinterId> printers) プリンタの検出を開始します。printersには検出済みのプリンタ情報が入っています。printersは補助的な情報です。このセッションで検出するプリンタとは関係ないので、printersに含まれるからといってプリンタ情報を追加しなくてよいわけではありません。プリンタの追加、更新はaddPrintersメソッドで行います。削除はremovePrintersメソッドで行います。
void onStopPrinterDiscovery() プリンタ検出を停止します
void onStartPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡を開始するように求めるコールバックです。印刷ダイアログでプリンタが選択された時に呼び出されます。プリンタの状態が変化した時はすみやかにシステムに通知しなければなりません
void onStopPrinterStateTracking(PrinterId printerId) プリンタの状態の追跡が終了した事を通知するコールバックです。
void onValidatePrinters(List<PrinterId> printerIds) 渡されたPrinterIdのリストが有効であるかの検証を求めるコールバックです。有効な場合はaddPrintersメソッドでプリンタ情報を更新する必要があります
void onDestroy() システムからこのセッションが不要であると判断された時に呼び出されます


 [リスト02]にPrinterDiscoverySessionの実装例を示します。この例は実際にDropboxPrintServiceで利用しています。onStartPrinterDiscoveryメソッドでプリンタを追加している以外は何もしていません。

 プリンタ情報はPrinterInfo.Builderを使って作成します。印刷可能な用紙サイズや、色、解像度などの設定を行えます。addPrintersメソッドでPrinterInfoのリストを渡せば、印刷ダイアログ上にプリンタ情報が表示されます。[リスト02]ではonStartPrinterDiscoveryメソッド内でPrinterInfoを作成し、addPrintersメソッドを呼び出していますが、通常はスレッドを起動してプリンタの検出を行い、Handler等を使ってプリンタ情報を追加する事になるでしょう。

リスト02 [PrinterDiscoverySessionの実装例]

public class DropboxPrinterDiscoverySession
  extends PrinterDiscoverySession {

  private final static String TAG = 
    DropboxPrinterDiscoverySession.class.getSimpleName();
  private final static String PRINTER_ID = "dropbox.print.service";

  private PrintService mPrintService;

  public DropboxPrinterDiscoverySession(PrintService printServie){
    mPrintService = printServie;
  }

  @Override
  public void onStartPrinterDiscovery(List<PrinterId> printers) {
    Log.d(TAG, "onStartPrinterDiscovery()");
    for (PrinterId id : printers) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
    List<PrinterInfo> addPrinters = new ArrayList<PrinterInfo>();
    PrinterId printerId = mPrintService.generatePrinterId(PRINTER_ID);

    PrinterInfo.Builder builder = new PrinterInfo.Builder(printerId,
        "Dropbox Printer", PrinterInfo.STATUS_IDLE);

    PrinterCapabilitiesInfo.Builder capBuilder =
      new PrinterCapabilitiesInfo.Builder(printerId);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_A4, true);
    capBuilder.addMediaSize(PrintAttributes.MediaSize.ISO_B5, false);
    capBuilder.addResolution(new PrintAttributes.Resolution(
        "Default", "default resolution", 600, 600), true);
    capBuilder.setColorModes(PrintAttributes.COLOR_MODE_COLOR
        | PrintAttributes.COLOR_MODE_MONOCHROME,
        PrintAttributes.COLOR_MODE_COLOR);

    builder.setCapabilities(capBuilder.build());
    addPrinters.add(builder.build());
    addPrinters(addPrinters);
  }

  @Override
  public void onStopPrinterDiscovery() {
    Log.d(TAG, "onStopPrinterDiscovery()");
  }

  @Override
  public void onValidatePrinters(List<PrinterId> printerIds) {
    Log.d(TAG, "onValidatePrinters()");
    for (PrinterId id : printerIds) {
      Log.d(TAG, "printerId:" + id.getLocalId());
    }
  }

  @Override
  public void onStartPrinterStateTracking(PrinterId printerId) {
    Log.d(TAG,
        "onStartPrinterStateTracking(printerId: "
            + printerId.getLocalId() + ")");
  }

  @Override
  public void onStopPrinterStateTracking(PrinterId printerId) {
    Log.d(TAG,
        "onStopPrinterStateTracking(printerId: "
            + printerId.getLocalId() + ")");
  }

  @Override
  public void onDestroy() {
    Log.d(TAG, "onDestroy()");
  }
}

onPrintJobQueued(PrintJob paramPrintJob)


 検出したプリンタに対して、ユーザが印刷を開始すると、onPrintJobQueuedメソッドにPrintJobが渡されます。PrintJobにはプリンタIDの他、印刷サイズや色情報、PDFファイルへのFileDescriptorなどが含まれています。印刷データの取り出しはFileDescriptorを使って行います。

 PrintJobは開始、完了、失敗、キャンセルなどのステータスを持っています。印刷の状態に応じてセットする必要があります。

 [リスト03]はPrintJobからPDFを取り出して保存する例です。onPrintJobQueuedメソッド上で全ての処理を行なっていますが、通常はAsyncTask等を使う事になるでしょう。ただし、PrintJobの操作はUIスレッド上で行わなければなりません。PDFデータのFileDescriptorも同様です。PDFデータをネットワーク上に送信する場合は、一時ファイル等を作成した上でAsyncTaskを利用する事になります。

リスト03 [PDFを保存する]

@Override
protected void onPrintJobQueued(final PrintJob printJob) {

  printJob.start();

  File tmp = new File(getExternalCacheDir(), printJob.getDocument()
      .getInfo().getName());

  FileOutputStream fos = null;
  FileInputStream fin = null;
  try {
    fos = new FileOutputStream(tmp);
    fin = new FileInputStream(printJob.getDocument()
        .getData().getFileDescriptor());
    int c;
    byte[] bytes = new byte[1024];

    while ((c = fin.read(bytes)) != -1) {
      fos.write(bytes, 0, c);
    }
    fos.close();
    fin.close();
  } catch (Exception e) {
    e.printStackTrace();
    printJob.fail(e.getMessage());
    return;
  } finally {
    if(fos != null){try{fos.close();}catch(Exception e){}}
    if(fin != null){try{fin.close();}catch(Exception e){}}
  }

  printJob.complete();

}

 PrintJobはPrintService内で自動的にキューイングされているので、改めて自分でキューのハンドリングをする必要はありません。アクティブなPrintJobはgetActivePrintJobsメソッドで取り出せます。

onRequestCancelPrintJob(PrintJob paramPrintJob)


 onRequestCancelPrintJobメソッドはユーザが印刷をキャンセルした時に呼び出されます。PrintJobが渡されるので、対応する処理を停止する必要があります。PrintJobクラスのisCancelledメソッドで確認する事もできます(リスト04)。

リスト04 [isCancelledメソッドを使う例]

protected void onPostExecute(PrintStatus result) {
    if(printJob.isCancelled()) {
        toast("キャンセルされました");
    } else if (result.isError()) {
        printJob.fail(result.getMessage());
    } else {
        printJob.complete();
    }
    toast(result.getMessage());
};

AndroidManifest.xmlに追加する


 作成したPrintServiceをAndroidManifest.xmlに定義する事で、 アプリケーションのインストール時にPrintServiceをシステムに追加できます(リスト05)。

リスト05 [AndroidManifest.xmlにPrintServiceを追加する]

<service
  android:name="app.package.name..YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>
</service>

設定画面を作る


 多くの場合、ネットワーク越しのプリンタに接続する為の設定がPrintServiceに必要となるでしょう。AndroidManifest.xmlでPrintServiceを定義する際にメタデータの設定を行えます。

 [リスト06]はPrintServiceで使う設定画面の定義例です。設定用のActivityを定義しています。XMLファイルは"res/xml"配下に作
成してください。

リスト06 [res/xml/your_printservice.xml]

<print-service
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="@string/description"
  android:settingsActivity="app.package.name.YourSettingActivity" />

 作成したXMLファイルをAndroidManifest.xmlのメタデータで指定します(リスト07)。これでPrintServiceの詳細画面から設定画面へ遷移できます。設定画面のActivityも忘れずAndroidManifest.xmlに定義しましょう。

リスト07 [AndroidManifest.xmlの設定]

<service
  android:name="app.package.name.YourPrintService"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
  <intent-filter>
    <action android:name="andraoid.printservice.PrintService" />
  </intent-filter>

  <meta-data
    android:name="android.printservice"
    android:resource="@xml/your_printservice" >
  </meta-data>
</service>

<activity
  android:name="app.package.name.YourSettingActivity"
  android:exported="true"
  android:label="@string/title_activity_setting"
  android:permission="android.permission.BIND_PRINT_SERVICE" >
</activity>

DropboxPrintServiceを作りました


 本エントリを書くに当たり、調査、習作としてDropboxにPDFを送信するPrintServiceを作成しました。使い方としては以下の通りです。

  • ソースはコチラ→DropboxPrintService
    • ソースの方にはAPIキーは含めていませんので、ソースからビルドして使う場合はDropboxでAPIキーを取得して下さい。
  • apkはコチラ→DropboxPrintService.apk


Dropboxにログインする


 DropboxPrintServiceをインストールしたら、まずはDropboxにログインする必要があります。SettingsのPrinting画面でDropboxPrintServiceを選択し、詳細画面を開いてDropboxPrintServiceをONにして下さい。下部のメニューから設定画面を開けるのでDropboxにログインして下さい。



[図03 Dropboxにログインする]

印刷する


 あとは普通に印刷するだけです。



[図04 Dropbox Printerを使う]

 PDFがDropboxにアップロードされます。



[図05 DropboxにアップロードされたPDF]

おわりに


 本エントリではPrintServiceの役割とざっくりした実装を解説しました。プリンタの追加、削除や、プリントジョブのキャンセル等をしっかり作りこむには色々と考慮点が多そうですが、基本的な事は本エントリの情報で実現できると思います。

 「印刷先がプリンタである必要はない」という点はとてもおもしろく、アイデア次第で便利なサービスを作れる気がします。Kitkatが普及するのはまだ先ですが、今後面白いPrintServiceが登場する事を期待しています。

2013年11月10日日曜日

Android 4.4 詳解 Printing Framework 前編

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 俺がPrinting Frameworkだ!という事で、「Android 4.4 KitKat 冬コミ原稿リレーを開催」の11/9担当のsys1yagiです。

 本記事では、Android 4.4で追加されたPrinting Frameworkで出来ることや、アプリケーションでPrinting Frameworkを使う方法などを解説します。内容はPrinting Contentをベースにしています。のちほどPrinting Contentの日本語訳も公開する予定です。

 今回の解説に登場するコードはGithubで公開しています。→PrintingFrameworkSample

 エミュレータとGalaxy Nexusに焼いたAndroid 4.4で動作確認しています。N5マジほしい。

 Printing Frameworkの概要


 Printing Frameworkは、Android端末からWi-FiやBluetoothやその他のサービスを介して接続したプリンタを使って、文書の印刷を可能にするフレームワークです。Kitkatを搭載した端末で以下の事が実現出来るようになります。

  • アプリケーションからPDFを出力して印刷する
  • PrintServiceを作って、プリントジョブを処理出来る

 注目すべきは「PDFを出力する」という点でしょう。アプリケーションでCanvasを使ってPDFのレンダリングが可能になります。

 PrintServiceは、アプリケーションが発行したプリントジョブを受け取って印刷処理を行うサービスです。PrintServiceを実装する事で、仮想のプリンタを作成したり、プリンタドライバとして振る舞ったりできます。標準ではStorage access framework(これもKitkatで追加されたAPIです)を通してPDFファイルをローカルストレージに保存するサービスが搭載されています。

 Printing Frameworkによる印刷処理の構成を以下に示します。


図01 [Printing Frameworkによる印刷処理の構成]

 印刷を行うには、アプリケーションでPrintDocumentAdapterを用意し、PrintManagerに対して印刷のリクエストを行います。PrintManagerは受け取ったPrintDocumentAdapterでPDFの作成を行い、印刷オプションのダイアログを表示します。印刷オプションを選択して印刷を実行すると、PrintJobがPrintServiceにキューイングされ、印刷処理が開始されます。これらのフローのうち、PDFを生成するPrintDocumentAdapterと、印刷処理を行うPrintServiceを任意に開発できます。

アプリケーションから印刷する


 アプリケーションからPrinting Frameworkを使って印刷をしてみましょう。Printing Frameworkは以下の3種類のフォーマットをサポートしています。それぞれのフォーマットの印刷方法を解説します。

  • 画像
  • HTML
  • カスタム

画像を印刷する


 Printing Frameworkの追加に伴って、support-v4にPrintHelperという画像の印刷処理を補助するクラスが追加されています(Support Libraryのrevision 19を参照)。PrintHelperクラスを使えば画像を印刷するためのPrintJobの作成や、PrintManagerの呼び出しを全て委譲でき、簡単に印刷処理を実現できます。

 PrintHelperではスケールモードとカラーモードの印刷オプションを設定できます。それぞれ2種類ずつオプションを持ちます。

表01 [PrintHelperのスケールモード]

項目 説明
SCALE_MODE_FIT ページの印刷領域に画像全体を表示するように納めます
SCALE_MODE_FILL ページの印刷領域の全体に画像をスケールします。この設定を選択すると、画像の上部と下部、または左右のエッジの一部が印刷されないことを意味します。このオプションがデフォルトとなります

表02 [PrintHelperのカラーモード]

項目 説明
COLOR_MODE_COLOR カラーで印刷します。このオプションがデフォルトとなります
COLOR_MODE_MONOCHROME グレースケールで印刷します


 以下のコードは、PrintHelperクラスを使って画像を印刷する例です。

リスト01 [PrintHelperで画像を印刷する]

private void printImage(String fileName, Bitmap bitmap) {
  if (PrintHelper.systemSupportsPrint()) {

    PrintHelper printHelper = new PrintHelper(context);
    printHelper.setColorMode(PrintHelper.COLOR_MODE_COLOR);
    printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT);
    printHelper.printBitmap(fileName, bitmap);

  } else {
    Toast.makeText(this, 
      "この端末では印刷をサポートしていません", 
      Toast.LENGTH_SHORT).show();
  }
}

 PrintHelperクラスのsystemSupportsPrintメソッドで端末が印刷をサポートしているか確認できます(と言っても内部ではBuild.VERSION.SDK_INT >= 19の結果を返却しているだけです)。printBitmapメソッドでファイル名とBitmapを渡したあとは何もする必要はありません。PrintJobが作成され、PrintManagerに対してリクエストが送られます。



図02 [印刷ダイアログの起動]

 PrintManagerに対してリクエストを送ると、図02の様に印刷ダイアログが起動されます。印刷ダイアログではプリンタの選択やコピー数、ページサイズ、カラー、印刷の向きなどの設定が行えます。



図03 [PDFを保存する]

 図03は「Save as PDF」でPDFを保存する例です。印刷を実行すると、保存先を選択する画面が起動されるます。ここではDownloadsにPDFを保存しています。



図04 [保存したPDFを開く]

 保存したPDFを開くと無事画像が出力されている事を確認できます。

HTMLを印刷する


 画像の印刷ではPrintHelperクラスを利用しましたが、画像以外を印刷する場合は自分でPrintManagerクラスに印刷のリクエストを送る必要があります。

 PrintManagerクラスに印刷リクエストを送るには、PDFのレンダリングを行うPrintDocumentAdapterクラスが必要となります。API Level 19でWebViewに追加されたcreatePrintDocumentAdapterメソッドを使うと、HTMLをレンダリングするPrintDocumentAdapterクラスを得られます。

 以下のコードはWebViewのcreatePrintDocumentAdapterメソッドでPrintDocumentAdapterクラスを取得し、PrintManagerクラスに印刷のリクエストを送る例です。

リスト02 [WebViewを使って印刷する]

private void printHtml(String fileName, WebView webView) {
  if (PrintHelper.systemSupportsPrint()) {

    PrintDocumentAdapter adapter = webView.createPrintDocumentAdapter();
    PrintManager printManager = 
      (PrintManager) getSystemService(Context.PRINT_SERVICE);
    printManager.print(fileName, adapter, null);

  } else {
    Toast.makeText(this,
        "この端末では印刷をサポートしていません",
        Toast.LENGTH_SHORT).show();
  }
}

 WebViewに予めWebページを表示しておき、上記コードを実行すると、以下の様に印刷が開始されます。



図05 [WebViewの内容を印刷する]

 WebページのPDFを保存できます。改ページもきちんとされています。



図06 [Webページを保存したPDF]

カスタムドキュメントを印刷する


 カスタムドキュメントを印刷する場合はPrintDocumentAdapterクラスを自分で実装します。PrintDocumentAdapterクラスは2つの抽象メソッドを持つ抽象クラスです。以下の様にサブクラスで抽象メソッドを実装する必要があります。

リスト03 [PrintDocumentAdapterクラスを継承する]

public class CustomDocumentPrintAdapter
  extends PrintDocumentAdapter {    

  @Override
  public void onLayout(PrintAttributes oldAttributes,
      PrintAttributes newAttributes,
      CancellationSignal cancellationSignal,
      LayoutResultCallback callback, Bundle extras) {

      //印刷オプション変更時に呼び出される。

  }

@Override
  public void onWrite(PageRange[] pages,
      ParcelFileDescriptor destination,
      CancellationSignal cancellationSignal,
      WriteResultCallback callback) {

      //PDF生成の処理をする

  };

 onLayoutメソッドは印刷ダイアログで、ページサイズや色などの印刷オプションを変更した時に呼び出されるメソッドです。LayoutResultCallbackクラスを使ってレイアウト変更に対するコールバックを行います。LayoutResultCallbackクラスのメソッドを以下に示します。

表03 [LayoutResultCallbackクラスのメソッド]

項目 説明
void onLayoutCancelled() ユーザあるいはアプリケーションからキャンセルを受けた事を通知します。キャンセルの状態はCancellationSignalクラスのisCanceledメソッドで確認できます
void onLayoutFailed(CharSequence error) レイアウト変更に対応できない場合にエラーを通知できます
void onLayoutFinished(PrintDocumentInfo info, boolean changed) レイアウト変更が完了した事を通知します。印刷情報を含んだPrintDocumentInfoクラスを作成して渡す必要があります。


 onWriteメソッドはページのレンダリング時に呼び出されます。PageRangeクラスは印刷する範囲が指定されています。PageRangeクラスの範囲に従ってParcelFileDescriptorクラスに対してPDFを書き出します。レンダリングの結果はWriteResultCallbackクラスを使って通知します。WriteResultCallbackクラスのメソッドを以下に示します。

表04 [WriteResultCallbackクラスのメソッド]

項目 説明
void onWriteCancelled() ユーザあるいはアプリケーションからキャンセルを受けた事を通知します。キャンセルの状態はCancellationSignalクラスのisCanceledメソッドで確認できます
void onWriteFailed(CharSequence error) PDFのレンダリングに失敗した場合にエラーを通知できます
void onWriteFinished(PageRange[] pages) PDFのレンダリングが完了した事を通知します


 以下のコードはPrintDocumentAdapterのサブクラスの実装例です。画像、タイトル、本文をコンストラクタで受け取り、印刷します。

リスト04 [CustomDocumentPrintAdapterの実装例]

public class CustomDocumentPrintAdapter extends PrintDocumentAdapter {

  private Paint mPaint = new Paint();
  private Context mContext;
  private Bitmap mBitmap;
  private String mTitle;
  private String mMessage;

  PrintedPdfDocument mPdfDocument;

  public CustomDocumentPrintAdapter(Context context, Bitmap bitmap,
    String title, String message) {
    mContext = context;
    mBitmap = bitmap;
    mTitle = title;
    mMessage = message;
  }

  @Override
  public void onLayout(PrintAttributes oldAttributes,
      PrintAttributes newAttributes,
      CancellationSignal cancellationSignal,
      LayoutResultCallback callback, Bundle extras) {

    mPdfDocument = new PrintedPdfDocument(mContext, newAttributes);

    if (cancellationSignal.isCanceled()) {
      callback.onLayoutCancelled();
      return;
    }
    int pages = 1;
    //newAttributes.getColorMode();
    //newAttributes.getMediaSize().getHeightMils();  //単位は1/1000インチ
    //newAttributes.getMediaSize().getWidthMils();   //単位は1/1000インチ

    PrintDocumentInfo info = new PrintDocumentInfo.Builder("androids.pdf")
        .setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
        .setPageCount(pages).build();
    callback.onLayoutFinished(info, true);
  }

 PDFのレンダリングはPrintedPdfDocumentクラスを使います。onLayoutメソッドのタイミングでPrintedPdfDocumentクラスを初期化しています。本来は印刷サイズに応じてページ数を計算するべきですが、ここでは割愛して1ページで固定しています。PrintDocumentInfoクラスを作成し、LayoutResultCallbackクラスのonLayoutFinishedメソッドに渡しています。

 次にonWriteメソッドを実装します。以下のコードではPrintedPdfDocumentクラスのstartPageメソッドでPdfDocument.Pageクラスを作成し、ページのレンダリングを行なっています。PdfDocument.PageクラスのgetCanvasメソッドでCanvasクラスを取得して任意にレンダリング処理ができます。PDFのレンダリングが完了したらParcelFileDescriptorに対してParcelFileDescriptorのwriteToメソッドでPDFの書き込みを行います。最後にWriteResultCallbackクラスのonWriteFinishedメソッドを呼び出せばレンダリングが完了します。

リスト05 [onWriteメソッドを実装する]

@Override
  public void onWrite(PageRange[] pages,
      ParcelFileDescriptor destination,
      CancellationSignal cancellationSignal, WriteResultCallback callback) {
    if (mPdfDocument == null) {
      return;
    }

    PdfDocument.Page page = mPdfDocument.startPage(0);
    if (cancellationSignal.isCanceled()) {
      callback.onWriteCancelled();
      mPdfDocument.close();
      mPdfDocument = null;
      return;
    }
    onDraw(page.getCanvas());
    mPdfDocument.finishPage(page);

    try {
      mPdfDocument.writeTo(new FileOutputStream(destination
          .getFileDescriptor()));
    } catch (IOException e) {
      callback.onWriteFailed(e.toString());
      return;
    } finally {
      mPdfDocument.close();
      mPdfDocument = null;
    }
    callback.onWriteFinished(pages);
  };

  public void onDraw(Canvas canvas) {
    //レンダリング処理
    //72で1インチとなる    
    //省略
  }
}


 作成したCustomDocumentPrintAdapterクラスを使ってみましょう。CustomDocumentPrintAdapterクラスを自分でインスタンス化する点以外はHTMLの印刷と変わりません。

リスト06 [CustomDocumentPrintAdapterを使う]

private void doPrint(){
  CustomDocumentPrintAdapter adaper = 
    new CustomDocumentPrintAdapter(context,
      BitmapFactory.decodeResource(getResources(),
      R.drawable.androids), "Hello Kitkat!!", getContext()
      .getString(R.string.long_message));

  printWithAdapter("custom.pdf", adapter);
}

private void printWithAdapter(String jobName,
  PrintDocumentAdapter adapter) {

  if (PrintHelper.systemSupportsPrint()) {

    PrintManager printManager = 
      (PrintManager) getSystemService(Context.PRINT_SERVICE);
    printManager.print(jobName, adapter, null);

  } else {
    Toast.makeText(this,
      "この端末では印刷をサポートしていません",
      Toast.LENGTH_SHORT).show();
  }
}

 PrintDocumentAdapterはCanvasを使ってPDFのレンダリングを行うので、インタフェースを工夫すればViewで利用する事ができます。ただしViewのCanvasとPDFのCanvasではスケールが異なるので注意して下さい。



図07 [PrintDocumentAdapterをViewで使う]

 印刷結果は以下の通りとなります。若干文字の大きさやレンダリング位置が異なります。



図08 [CustomDocumentPrintAdapterでレンダリングしたPDF]

おわりに


 PDFをレンダリング出来るようになったのはとてもエキサイティングですね。ただPDFの読み込みはサポートしていないので、エミュレータなどPDFを表示するアプリが入っていない環境でSace as PDFすると中身が確認できない点がつらいです。

 後編ではPrintServiceの仕組みと実装方法を解説します。DropboxにPDFを出力するDropboxPrintServiceを作ったのでそちらも合わせて解説&公開したいと思います。

2013年9月18日水曜日

Android SDK tools 22.2で新しいプロジェクトが作成できない問題を直す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 昨日Android SDKがアップデートされたようです。Android SDK tools 22.2。すると新規プロジェクト作成の際ウィザードで以下のエラーが出て進めなくなるそうです。



 このエラー最近みたなーCode Template触ってるときになー。あー。template.xmlだろうかねー、という事で色々見たら動く様に出来たのでメモっておきます。Code Templateってなにという方は「Using Code Templates」を読むといいんじゃないでしょうか。

Issueにもあがってるしすぐ直ったバージョンが降ってくると思いますが、緊急の場合はまーやったらいいと思います。


Eclipse+ADTの場合


 結論から言うとCode Templateのtemplate.xmlが腐っています(正確にはCode TemplateのアップデートにADT plugin側が追い付いていないぽい)。Code TemplateはSDKの以下の場所にあります。

$ANDROID_HOME/tools/templates

 いっぱいあるんですけど、ここでは"BlankActivity"と"FullscreenActivity"だけ見ます。これらはそれぞれ以下のパスにあります。

$ANDROID_HOME/tools/templates/activities/BlankActivity
$ANDROID_HOME/tools/templates/activities/FullscreenActivity

BlankActivityのtemplate.xmlを直す


 まずはBlankActivityから。template.xmlの中身を見ると・・・。

$ANDROID_HOME/tools/templates/activities/BlankActivity/template.xml

 以下の様になっています(冒頭だけ抜粋)。この内、問題となるのはdependency要素です。name属性に"appcompat"、version属性に"v7"とあります。dependency要素はテンプレートの依存性を記述する為のものですが、今までは"android-support-v4"しかサポートしてませんでした(InstallDependencyPage.javaにもandroid-support-v4の名前が直書きされており、コメントにもオンリーやでと書かれています)。

<?xml version="1.0"?>
<template
    format="3"
    revision="3"
    name="Blank Activity"
    description="Creates a new blank activity, with an action bar
     and optional navigational elements such as tabs or horizontal swipe.">

    <dependency name="appcompat" version="v7" />

    <category value="Activities" />

 name="appcompat"という事はappcompatをサポートするんだなーActionBarとかNavigation Drawerのテンプレートが追加されるんだろーなーという期待を持てますが今回のリリースには入ってなかったようで「appcompatってなんだよあーん?」とエラーになるみたいです。

 dependency要素を以下の様に書き換えるといつも通りに使える様に戻ります。

<dependency name="android-support-v4" revision="8" />


FullscreenActivityのtemplate.xmlを直す


 次にFullscreenActivityのtemplate.xmlを見ると、dependency要素が二つあります。name="support"ってなんやねん!という事で、上のname="support"の方を消せば動きます。
 
<?xml version="1.0"?>
<template
    format="3"
    revision="3"
    name="Fullscreen Activity"
    description="Creates a new activity that toggles the visibility of the system UI (status and navigation bars) and action bar upon user interaction."
    minApi="4"
    minBuildApi="16">
    <dependency name="support" version="v4" /> ← 消す
    <dependency name="android-support-v4" revision="8" />

    <category value="Activities" />

Android Studioの場合


 あーもうフラグメントの波が・・・。めんどうなのでまだ書きません。現象としてはBlankActivityでNavigation Typeを"Fixed Tabs + Swipe"とかにしても、"None"のソースが吐かれたりします。Android StudioもCode Templateを内包しているので、そこを直せば動くと思いますがめんどうなのでやってません。

おわりに


9月の末に発売予定の「Effective Android 正式版」にCode Templateのあれこれを書いているので是非〜

 

2013年9月16日月曜日

Genymotionのエミュレータはマルチタッチをサポートしていた

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
 Genymotionはサックサクでいいなー、でも今のところ4.1.1以上のエミュしかないっぽいので、古いの試す時めんどいなー、とか思いつつGenymotionの公式をちらほら見ていたら「マルチタッチ出来るぜ!」みたいな事が書いてあって驚いたので試したらマジで動いたので書く。

Multi-touch Gestures


 Android SDKの「Android Virtual Device Manager」で作って動かすAndroidのエミュレータはいまだにマルチタッチのサポートをしていない!→だから辛い!→iOSのシミュなら出来るのに!→でもまーエミュ使わないしいいか!→Genymotionはエミュのくせに早い!→マルチタッチやっぱほしい!→「出来るで」→(^q^)

 User Guideの"Genymotion Application"の所に"Multi-touch Gestures"という項目があります。とりあえず、操作部分の解説を以下に訳しておきます。

右クリック + マウスを左に移動: ズームイン
右クリック + マウスを右に移動: ズームアウト
右クリック + マウスを上に移動: 上方向の二本指スワイプ
右クリック + マウスを下に移動: 下方向の二本指スワイプ
SHIFT + 右クリック + マウスを左に移動: 反時計周りの回転
SHIFT + 右クリック + マウスを右に移動: 時計回りの回転

※: Mac OS X系の場合、「右クリック」を「CTRL + クリック」に置き換えて下さい。

Multi-touch Gesturesを試す


 早速試してみます。Genymotionのエミュのブラウザでブログを開いて、ズームイン操作をすると・・・。


 すっげマジうごいたすっげ。

おわりに


 とりあえずタッチイベントをチラ見したら当然ちゃんとイベント来てました。座標系とかタイミングとかどうなるのか細かく検証はしてないですけど、簡単なマルチタッチ操作の動作確認はエミュで出来そうです。いやーえらい。Genymotionえらい。

2013年9月11日水曜日

Android Studio 0.2.7をビルドしてみた(Macで)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
Android Studioをキッチリ触っていこうかなーと思っていたら、なんかこの辺り(http://tools.android.com/build/studio)に「Android Studioのソースはここやで」、とか書いてあったので早速ソース落としてビルドしたら簡単だったのでメモっておきます。

ソースを取得する


 ソースはrepo initでリポジトリの設定してrepo syncするだけです。もしrepoが無い場合は以下を参照して入れて下さい。

Downloading the Source :
http://source.android.com/source/downloading.html

 Android Studioのソースをダウンロードする為のディレクトリを作り、そこで以下のようにrepo initします。そしてrepo sync。

mkdir aosp; cd aosp
repo init -u https://android.googlesource.com/platform/manifest -g tools
repo sync

 これでどさーっとAndroid Studio関連のソースが落ちてきます。Android Studioのソースは以下のパスにあります。
aosp/tools/idea/

ビルドする


 ビルドは何か色々いるのかと思ったら凄く簡単でした。jdk1.6以上とantが入っていればいける様です。
以下のディレクトリに移動してantを叩くだけです。ビルドが始まります。
cd tools/idea
ant

 ビルド後以下のパスに各プラットフォーム向けのバイナリのzipファイルが生成されます。

aosp/tools/idea/out/artifacts/android-studio-SNAPSHOT.mac.zip

起動してみる


 android-studio-SNAPSHOT.mac.zipを解凍すると・・・



 すげーまじでAndroid Studio.appが出来ているーー。という事で起動してみます。



 無事起動できました。バージョンを見ると0.2.7です。

おわりに


 なんかバグがあったり気に入らない部分あったら自分で書き換えたらいいんじゃないでしょうかー(うつむきながら)
ソースチラ見したけど何がどーなってるのかじぇんじぇんわかりませんでした。

2013年8月6日火曜日

ディーペスト問題の星間直行便のソーシャルグラフをarbor.jsで描いたよ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

 先日CodeIQで結城 浩さんが出題していた星間飛行ルートを作ろう!に挑戦したのでその解答をポストします。あとついでに今回の問題で使われたデータをarbor.jsを使って可視化したのでそれも載せておきます。
 
 結果は満点の5点でした。わーい。

問題の概要


 今回の問題は、特定のポイントを経由し、目的地までたどり着くルートを算出するというものでした。ルートはWeb APIから1件ずつしか取得出来ない仕様となっており、また取得出来るデータはA->Bという一方向のルートとなります。解き方としては、startの星から行ける星のデータをWeb APIから取得して、その星から行ける星のデータを取得して…という操作を繰り返して特定のポイントを経由して目的地にたどり着く形となります。

利用言語、環境について


 今回は普段使ってない言語でやろう、という事でRubyを使いました。問題を解くのに必要なのはhttp通信周りや配列操作だけだったので特に問題なく解けました。

処理概要とソース


 処理としては指定した星から行ける直行便のリストを取得して、その中にゴールがあるまで読み込み続けるというものでした。Routeクラスを作り、各ルートをお互い繋げる事で、startから目的地までのルート情報を保持出来る様にしています。大体30分程度で書きました。
 
require 'net/http'

#start 5426528869786
#deep 4363616111476
#deeper 5092488161056
#deepest 8838746292440

class DirectFlightRoute
  @id
  @routes
  attr_accessor :id, :routes
end

class Route
  @prev
  @current
  def initialize

  end
  attr_accessor :prev, :current
end

class RouteResolver
  @@API_ROOT = "http://133.242.134.37"
  @@API_PATH = "/deepest.cgi?"
  @@ID = "id"
  @@NTH = "nth"

  @allRouteData
  attr_accessor :allRouteData
  def initialize
    @allRouteData = []
  end
  def getDirectFlight(id, nth)
    url = URI.parse(@@API_ROOT)
    req = Net::HTTP::Get.new(@@API_PATH + @@ID + "=" + id + "&" + @@NTH + "=" + nth.to_s)
    res = Net::HTTP.start(url.host, url.port) {|http|
      http.request(req)
    }
    if res.code.eql?("200")
      return res.body.chomp()
    end
    return "-3"
  end

  def getDirectFlightList(id)
    result = []
    nht = 0
    while true
      df = getDirectFlight(id, nht)
      puts "df:"+nht.to_s + ":" + df
      if "0".eql?(df)
        break
      end
      if !"-3".eql?(df)
        result.push(df)
        nht += 1
      else
        puts "error -3"
      end
      sleep 1
    end
    puts id+":"+result.to_s
    directFlightRoutes = DirectFlightRoute.new
    directFlightRoutes.id = id
    directFlightRoutes.routes = result
    @allRouteData.push(directFlightRoutes)
    return result
  end

  def isConflictRoute?(node, id)
    while node.prev != nil
      prev = node.prev
      if prev.current == id
        return true
      end
      node = prev
    end
    return false
  end

  def isAlready?(id)
    #答えのルートを得る場合は以下4行をコメントアウトする
    #@allRouteData.each{|route|
    #  if route.id.eql?(id)
    #    return true
    #  end
    #}
    return false;
  end

  def resolveRoute(start, goal)
    result = []
    candidates = []
    startRoute = Route.new
    startRoute.current = start
    candidates.push(startRoute)
    while !candidates.empty?
      candidate = candidates.pop()
      directFlightList = getDirectFlightList(candidate.current)
      directFlightList.each{|df|
        #ゴールに到達した?
        #※全ルートを得る場合はコメントアウト
        if goal.eql? df
          node = candidate
          result.unshift df
          result.unshift node.current
          while node.prev != nil
            n = node.prev
            result.unshift n.current
            node = n
          end
          return result
        end
        #既に登場したか?
        if !isConflictRoute?(candidate, df)
          if !isAlready?(df)
            newRoute = Route.new
            newRoute.current = df
            newRoute.prev = candidate
            candidates.push(newRoute)
          end
        end
      }
    end
    return result
  end

  def resolve(routes)
    result = ["dummy"]
    routes.each{|r|
      route = resolveRoute(r[0], r[1])
      result = result.slice(0, result.size-1).concat(route)
    }
    return result
  end
end

routeResolver = RouteResolver.new
results = routeResolver.resolve([["5426528869786", "4363616111476"],["4363616111476", "5092488161056"],["5092488161056","8838746292440"]])
puts "result-------------------------------"
puts results.to_s

得られた結果


 上記プログラムで以下のルートデータを取得する事が出来ました。想定された答えより大分ながーいルートです。手当たり次第で探索したので無駄なルートを沢山通っています。

["5426528869786", "2484466449149", "4326837688991", "6021351385219", 
"8814668496374", "1074912512567", "7560374248806", "8217940346560",
 "6159521237181", "6248392538888", "5792719602457", "7713050646130",
  "6493203887111", "3014332099928", "4363616111476", "2615115005762", 
  "3492704769369", "6101275938556", "5793542169547", "4217007177539", 
  "2970040126310", "6218636660558", "1563577571047", "8742972189444", 
  "8433634935614", "8564585174926", "6730551897632", "8279347398297", 
  "5813746423067", "3459654931377", "8244481309445", "8279056819547", 
  "8054128267676", "7401422935060", "5099100039591", "5471056644598", 
  "7892247098840", "8479078158931", "8986753372139", "6137149535463", 
  "7934869824134", "7782150475311", "4498551666547", "6023249450084", 
  "7014829010728", "7322686465928", "5702278445116", "2864141700776", 
  "8735695204612", "5174811483802", "5873652513502", "3536542280812", 
  "6178493999671", "8188288894326", "7797971735921", "3971331745645", 
  "5485131078541", "3876548341627", "1588698186896", "4656064657053", 
  "3065937285535", "3437245100188", "5833671447983", "6714942513619", 
  "7049674030681", "5813482662518", "2547413633555", "2966922227746", 
  "1843019516033", "5092488161056", "8250347815782", "4775196002726", 
  "8784346813978", "8636102321011", "8345370958267", "8787239992833", 
  "7230764147984", "8701256641199", "8682011082922", "8225271457185", 
  "8179247968273", "8573279739883", "8025149145454", "7840395069634", 
  "6925571750184", "7706339201843", "6159786607508", "8108466497204", 
  "6912947983983", "6870921135953", "6931349063153", "5848253839570", 
  "7170473222416", "8033311672835", "7194628242299", "6762287044283", 
  "8526665430645", "7116209295800", "7652884912454", "3101910152403", 
  "5985346038182", "8741714209435", "6199888663851", "4597865188910", 
  "5692390052367", "3545866022750", "6449164628142", "8978032168895", 
  "4946412662661", "3840068516051", "4205606644588", "5256084349572", 
  "6201473389311", "6146124596038", "3943178099168", "7758274483738", 
  "6085641567012", "5596295380961", "6707581077767", "5717371807428", 
  "6040500266078", "2109165486888", "5260999957124", "4491504900204", 
  "2223936654310", "3575202947166", "3070747769765", "2812280975542", 
  "4940548870566", "5576554716124", "2152647356070", "2994428026714", 
  "3031196364476", "5591596393385", "5865952095146", "8838746292440"]

直行便データを全て取得してソーシャルグラフを描く


 なんか適当にクロールしてたらゴールにたどり着いたので、折角だし星間の関係を可視化したいよなと思って、上のスクリプトを少しいじって全ての星の直行便データを取り出してグラフ書こうと考えたわけです。

arbor.jsでグラフを描く

 
 得られたデータは1162件ありました。このデータをarbor.jsにちょろっと食わせると良い感じにグラフ化してくれます。実際arbor.jsで動くものも見れます。 (※動作超重い可能性があるので注意して下しあ)
http://ec2-54-214-204-32.us-west-2.compute.amazonaws.com/codeiq_deepest/



仕掛けられた罠達


 グラフを見てみるとループする参照の仕方をする星達がごちゃっと塊になった領域がありますね。動く方を追いかけるとわかりますが、deepとdeeperの間にある"1814964223463"も袋小路になってドサッと発散してるのが分かります。
 
  • ループする星たち
  • 相互参照する星に突き当たるとループしてしまう事になります。既に通った星に行くルートは除外する処理を入れる必要があります。
  • 発散する星
  • 例えば"1814964223463"とかいう星はnthをいくら大きくしても終端を表す"0"が返ってきません。グラフの方では花の様になっている星が発散する星です。他の星は大体10件程度しか直行便が無いので、取得数の制限をすれば回避する事が出来ます。
     
    図を見た上で探索する方法を検討する分には割と簡単ですが、APIだけしか公開されていない状態だとなかなか気付けないかもしれないですね。


    おわりに


     おもろかった。

    2013年6月26日水曜日

    自分用Google ReaderをNode.js+MongoDBで作成。オープンソースだよ。

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

    はじめに


     なんかGoogle Readerが6末に終わるらしいんであー代替ねー。めんどいなー。自分で作るかー。という感じです。完全に俺得です。せっかくなんでNode.jsとか使うかーと。でDB周りはMongoDBでええかーとか適当に。勉強がてら。粛々と。のんびりと。

    紹介


     Google Readerの代替という事でどういう名前にしようかと思ったんですけどまぁ"やぎ"だし
    Goat Readerでいいやという事にしました。とりあえず触れる環境があるんで、メンドイ方は以下を見て下さい。モバイルにはまだ対応してません。Twitterでログインして使ったりも出来ます。ログインしていない状態でアクセスするとAnonymouseモードとなります。


    できること


     繰り返しになりますが適当に作った上自分用なので完全に俺得機能しか乗せる気がありません。更に別に完成してないんでまぁアレです。今のところできることは大体以下です。

    • Twitter認証でのユーザ登録
    • RSSフィードの登録
    • RSSフィードの削除
    • Google Readerのsubscriptions.xmlのインポート
    • 登録したRSSフィードのクロールして未読アイテムを追加
    • 表示モード2つ

    今後追加するかなーという機能としては以下です。
    • ふぁぼ、あとで読む機能
    • 未読、既読、すべて表示切り替え
    • 新しい順、古い順の表示
    • TwitterのTLのURL付きツイートをフィード化
    • TwitterのTLからhotを抽出
    • フィードへのカテゴリ設定
    • アイテムへのタグ追加
    • アイテムへのコメント追加
    • モバイル対応
    • Typescript化する
    • gruntを導入する

    ソース


     ソースはGithubで公開しています。pullreq歓迎です。色々まだ足りてません。
    sys1yagi/Goat-Reader

    使う


     単純に使うだけなら簡単です。Twitterでログインして、Settingsにフィード登録してしばーらく待つとアイテムが増えているはずです。Google Readerのsubscriptions.xmlのインポートも概ね出来ます。

    導入の手順


     README.mdを参照して下さい->https://github.com/sys1yagi/Goat-Reader

    内部的な奴


     チョイスは結構適当なんですが、まぁとりあえずnpmやbowerで持ってこれるヤツを中心に利用しています。とりあえず列挙してみます。

    フロントエンド


    モジュール 説明
    bootstrap.css Twitter社が提供してる良い感じのCSSですね。便利。
    jquery 言わずと知れたjQuery。言うまでもないっすね。モバイル向けにはZeptoを使うと思います。
    flight Twitter社製のコンポーネントベースのフレームワークです。各DOMの処理はこのFlightのコンポーネントを使って動作させています。
    requirejs Flightを使う為に必要なので入れてます。CommonsJSの何かの実装だった気がします。AMDかな?この辺はFirefox OSなどHTML5製アプリを作る上で必須なパラダイムだよねーと。
    es5-shim これもFlightを使う為のライブラリです。ECMAScript5の機能の一部を実装しているんだとか。
    hogan Twitter社が提供しているライブラリで、scriptタグの中にHTMLテンプレートを記述して、jsからDOM生成に使えるというヤツです。JSPとかPHPに似てるかも。
    datejs 日付の文字列フォーマットを良い感じにするライブラリです。DateFormatくらいは標準で欲しいんだけどなぁ・・・

    バックエンド


    モジュール 説明
    Node.js nodeかわいいよnode
    MongoDB mongoかわいいよmongo
    mongoose Node.jsからMongoDBに良い感じにアクセスする為のモジュールです。JSON形式でテーブルスキーマを定義してモデル作ったり出来ます
    express Node.jsのアプリケーションフレームワーク。割と色々やってくれる。助かる。
    jade HTMLテンプレエンジン。文法が独特でちょっとつらい。HTML2Jadeとかで変換して使ったりした
    fibers 非同期処理系をブロッキングする為のモジュール。Node.jsは非同期コールバックネスト地獄に陥りやすいのよね。
    cron Node.js内でクロール処理をさせるのに使った。まんまcron
    jquery サーバサイドでも活躍!でもテスト動作させる為にしか使ってないかも?
    mocha テスティングフレームワーク。あんまりテスト書いてないけど。ブロッキングの要るテストが書きやすいのでjasmineやめてこちらにした。
    passport TwitterのOAuthに使った。これは便利。Facebookとかもあるらしい
    passport-twitter これな。
    xml2json XMLをjsonに変換してくれる。助かる。
    forever Node.jsのプロセス管理ツール。daemon化したりあれこれ

    注意点


    ・スケールとか全く考えてないよ。ユーザ増えると爆死する気がするので自分で鯖立てて個人利用を推奨します。

    終わりに


     基本的に"Node.jsでアプリ作りたい"というのがメインの動機です。で、Node.js使った感想は"剥き出しだなぁ"と。Pythonのmod_pythonだとかと同じでリクエストハンドラと云々がアレ程度にしか用意されてないんで上に色々作る必要があるなぁと。Express使ってもJadeと連携とかSession StoreがどうとかFile uploadが便利とかはあるけど、リクエストハンドラ周りとか自分で色々アレしたんでまだまだかなーと。でも多分既にその辺カバーするフレームワーク出てるだろうなーと。いずれその辺に対応していくかもしれないなーと。

     npmやbowerで色々持ってくる仕組みはとてもおもろいと感じた。これってMavenとかGradleにかなり近いはずなんであーMaven使っておけばなぁとか思った。これからはGradleの時代なのでGradleでJava系プロジェクトは扱うようにしていきたいなぁーとか。今回はnpmとbowerを個別で色々触っていたけど次回何かやるならYeomanを使おうかなと思う。

    で、

    やっぱプログラミングって楽しいなぁ

    と思った。忙しくて大変だったけど。

    2013年6月7日金曜日

    シャフラーズ問題の回答 @sys1yagi編

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

     CodeIQで結城 浩さんが出題していた断片情報からデータを復元しよう!に挑戦したので解答をポストします。結果は満点でした。よかつた。今回はすごく簡単でした。30分程度で書き散らかしたのでソースは汚いです。
     

    問題


     問題は以下の様なデータから、 aa = 10とかいった風にペアのデータを突き止めろ、というモノでした。てっきりデータ数が膨大だったりするのかと思ってたら1000件程度。サクーッといけました。
     10 22 24 = aa bb cc
     53 33 10 = dd ee aa
     24 33 53 = bb ee dd
    
     

    解答ソース


     上記フォーマットのデータファイルを食わすと、ペアを解析してprintlnする感じのソースです。最初データを見たとき、「連立方程式っぽいな」と思ったんでそういう感じで解くようにしました。
     10 22 24 = aa bb cc
     53 33 10 = dd ee aa
     24 33 53 = bb ee dd
    
    というデータがあった時、aaに着目すると、1行目ではaaの候補は10, 22, 24になります。
    んで、2行目と比較すると53, 33, 10のうち10だけが1行目に存在してます。つー事でaa=10だ!という事。そんだけです。
     10 22 24 = aa
     53 33 10 = aa
     ↓重複する数字だけ取り出す
     10 = aa
    
     あとはデータを読み込んでkey,valueのリストを保持するShufflerクラスのリストを作って、名前の一覧を抽出し、名前に着目しながら1件ずつ特定していく感じ。特定された名前と数字はShufflerクラスにフィードバックして、Shuffler内でそれらの値をリストから削除する、という構造です。今見返すと実装大分汚いですが、大体そんな感じです。

    package jp.dip.sys1.shufflers;
    
    import java.io.BufferedReader;
    import java.io.FileInputStream;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    public class Main {
        static class Shuffler {
            private List<String> mIdList = new ArrayList<String>();
            private List<String> mNameList = new ArrayList<String>();
    
            public Shuffler(String[] ids, String[] names) {
                for (String id : ids) {
                    this.mIdList.add(id);
                }
                for (String name : names) {
                    this.mNameList.add(name);
                }
            }
    
            public List<String> getIdList() {
                return mIdList;
            }
    
            public List<String> getNameList() {
                return mNameList;
            }
    
            public void found(String id, String name) {
                mIdList.remove(id);
                mNameList.remove(name);
            }
            public User getUser(){
                if(mIdList.size() == 1 && mNameList.size() == 1){
                    User user = new User(mIdList.get(0), mNameList.get(0));
                    found(mIdList.get(0), mNameList.get(0));
                    return user;
                }
                return null;
            } 
            @Override
            public String toString() {
                if(mIdList.isEmpty() && mNameList.isEmpty()){
                    return null;
                }
                StringBuilder sb = new StringBuilder();
                for (String id : mIdList) {
                    sb.append(id + " ");
                }
                sb.append("= ");
                for (String name : mNameList) {
                    sb.append(name + " ");
                }
                return sb.toString();
            }
        }
    
        static class ShufflerFactory {
            public static Shuffler createShuffler(String line) {
                if (line == null) {
                    return null;
                }
                String[] pair = line.split(" = ");
                String[] ids = pair[0].split(" ");
                String[] names = pair[1].split(" ");
                Shuffler shuffler = new Shuffler(ids, names);
    
                return shuffler;
            }
        }
    
        static class User {
            private String mId;
            private String mName;
    
            public User(String id, String name) {
                mId = id;
                mName = name;
            }
    
            public String getId() {
                return mId;
            }
    
            public String getName() {
                return mName;
            }
    
            @Override
            public String toString() {
                return mId + " = " + mName;
            }
            @Override
            public int hashCode() {
                return toString().hashCode();
            }
            @Override
            public boolean equals(Object obj) {
                return this.hashCode() == obj.hashCode();
            }
        }
    
        static class Matcher {
            private String mTargetName = null;
            private List<String> mCandidateIds = new ArrayList<String>();
            private User mUser = null;
    
            public Matcher(String targetName) {
                mTargetName = targetName;
            }
            public boolean identify(Shuffler shuffler) {
                for (String name : shuffler.getNameList()) {
                    if (mTargetName.equals(name)) {
                        if (mCandidateIds.isEmpty()) {
                            for (String id : shuffler.getIdList()) {
                                mCandidateIds.add(id);
                            }
                        } else {
                            List<String> newCandidateIds = new ArrayList<String>();
                            for (String id : shuffler.getIdList()) {
                                if(mCandidateIds.contains(id)){
                                    newCandidateIds.add(id);
                                }
                            }
                            mCandidateIds = newCandidateIds;
                        }
                    }
                }
                if(mCandidateIds.size() == 1){
                    mUser = new User(mCandidateIds.get(0), mTargetName);
                    return true;
                }
                else{
                    return false;
                }
            }
    
            public User getUser() {
                return mUser;
            }
        }
    
        List<Shuffler> loadShufflers(String path) {
            List<Shuffler> list = new ArrayList<Main.Shuffler>();
            FileInputStream fis = null;
            InputStreamReader isr = null;
            BufferedReader br = null;
            try {
                fis = new FileInputStream(path);
                isr = new InputStreamReader(fis);
                br = new BufferedReader(isr);
                String line = null;
                while ((line = br.readLine()) != null) {
                    Shuffler shuffler = ShufflerFactory.createShuffler(line);
                    if (shuffler != null) {
                        list.add(shuffler);
                    } else {
                        System.out.println("error.");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    br.close();
                    isr.close();
                    fis.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return list;
        }
    
        List<String> treatName(List<Shuffler> list) {
            List<String> names = new ArrayList<String>();
            for (Shuffler shuffler : list) {
                for (String name : shuffler.getNameList()) {
                    if (!names.contains(name)) {
                        names.add(name);
                    }
                }
            }
            System.out.println(names.size());
            return names;
        }
    
        public void matching(String path) {
            List<Shuffler> list = loadShufflers(path);
            List<User> users = new ArrayList<Main.User>();
            List<String> names = treatName(list);
            for (String name : names) {
                Matcher matcher = new Matcher(name);
                for (Shuffler shuffler : list) {
                    if (matcher.identify(shuffler)) {
                        break;
                    }
                }
                User user = matcher.getUser();
                if (user != null) {
                    if(!users.contains(user)){
                        users.add(user);
                    }
                    for (Shuffler shuffler : list) {
                        shuffler.found(user.getId(), user.getName());
                    }
                }
            }
            for(Shuffler shuffler : list){
                User user = shuffler.getUser();
                if(user != null){
                    if(!users.contains(user)){
                        users.add(user);
                    }
                    for(Shuffler shuffler2 : list){
                        shuffler2.found(user.getId(), user.getName());
                    }
                }
            }
            Collections.sort(users, new Comparator<User>() {
                @Override
                public int compare(User o1, User o2) {
                    return Integer.parseInt(o1.getId()) - Integer.parseInt(o2.getId());
                }
            });
            for (User user : users) {
                System.out.println(user);
            }
            List<String> remains = new ArrayList<String>();
            for(Shuffler shuffler : list){
               String remain = shuffler.toString();
               if(remain != null){
                   if(!remains.contains(remain)){
                       remains.add(remain);
                   }
               }
            }
            for(String remain : remains){
                System.out.println(remain);
            }
        }
        public static void main(String[] args) {
            Main main = new Main();
            //main.matching("shufflers/sample.txt");
            main.matching("shufflers/shufflers.txt");
        }
    }
    

    2013年5月24日金曜日

    ピッグデータ問題の解答 @sys1yagi編

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
     先日CodeIQで結城 浩さんが出題していた《ピッグデータ》に負けないで! に挑戦したのでその解答をポストします。以下の内容はほぼ提出した解答そのままです。4/30の夜中に挑戦を開始して、翌日5/1の夕方辺りに解答を提出しました。
     
     結果は正解!「評価5 ベスト・ピッグデータ賞(結果が正しく技術メモも十分な解答)」だそうです。わーい。結城さんの解答も同じ考え方だったので二度わーい。

    はじめに


     本書は、「ピッグデータ問題」のドキュメントである「problem.txt」、「pigdata.pdf」を読み、ピッグデータの仕様について理解している事を前提とした技術メモです。「ピッグデータ問題」の詳細については「《ピッグデータ》に負けないで! https://codeiq.jp/ace/yuki_hiroshi/q303」を参照して下さい。

    利用言語、環境について


     言語はjavaを利用する事にしました。標準ライブラリにSHA-1によるハッシュ計算を行う事が出来るMessageDigestクラスが存在する点と、マルチスレッド処理が容易な点、実行速度がスクリプト言語より優位と考えられる点から選択しました。
     実行の環境は以下の通りです。

  • OS: OS X 10.8.3(Mountain Lion)
  • CPU: 1.8 GHz Intel Core i7 (4コア)
  • メモリ: 4GB
  • java: 1.8.0-ea (1.6系、1.7系でも問題なく動作は可能のはず。たまたま入っていたのでそのまま利用した)


  • getsign(107374182400, 16777216)の答え


    209679232

    処理概要


     getdataでは、SHA-1によるハッシュ計算のインプットとなるqがindexを10で除算した値である事から、qが変化するまでハッシュ値を保存する事によってハッシュ値計算の回数が減少する様にしています。getsignではExecutorServiceを利用しマルチスレッドによって同時に計算処理をする構造になっています。getsignで必要な指定count数のピッグデータの取得とソートについては、65536(16bit)個のlong配列を使って、getdataで得られた値の個数をカウントする方式としました。この処理方式に至った経緯の詳細については"処理詳細と工夫について"を参照して下さい。

    処理詳細と工夫について


    オンメモリの処理ではメモリが足りない問題


     当初getsignでは、与えられたcountの個数だけgetdataを実行し、結果をArrayListに格納していました。count個の値が出揃った後ArrayListをソートし、skipsに従ってシグネチャを計算しました。getsign(100, 10)の場合は問題無く動作しましたが、count個のデータをArrayListに格納するので、getsign(107374182400, 16777216)では当然メモリが足りなくなります。この方法ではgetsign(107374182400, 16777216)は計算出来ない事が分かりました。

    データを保存する場合2600GB必要になる


     オンメモリで大規模なgetdataの値を保持する事は困難である事から、外部記憶装 置にindexから得られるピッグデータを保存する事を考えました。後ほどソートが必要となる為、データは以下の様なリンクドリストの性質を持つ構造にしました。

    index(64bit), ピッグデータ(16bit), 前のindex(64bit), 次のindex(64bit)

     1データ辺り64+16+64+64=208bit=26byteとなり、これを107374182400個保存する場合、107374182400*24=2600GBの外部記憶装置が必要となります。2600GB=2.6TBは今ではそこまで大きなサイズとは言えませんが、2.6TBの読み書きとなると膨大な時間がかかる事が予想されます。その他、リンクドリストという構造上skipsとkに従ったシグネチャ計算を行う場合全データにシーケンシャルにアクセスする必要があったり、データサイズの大きさから実行可能な環境に大きな制約を与える事になります。この方式も見送る事にしました。

    ピッグデータが16bitである点に着目


     ピッグデータはpigdata.pdfの仕様から、符号なしの16bitの整数です。つまり0-65535の範囲の計65536種類という事になります。そこで、65536個の配列を作成し、getdataから返されたピッグデータを添字にして以下の様にgetdataから得られたピッグデータの取得回数を数え上げる事で省メモリ化出来ると考えました。

    int[] counter = new int[65536];
    int data = getdata(i);
    counter[data]++;
    

     また、この方式によりgetdataで得た一連の値のsortが不要となります。counterにアクセスする為の添字はgetdataで得られるピッグデータで、その添字でアクセスするcounterの値はgetdataで得られたピッグデータの回数を表します。添字を0から65535まで数え上げながら個数を足し合わせる事でソート済みのgetdataの配列と同等の情報が得られます。以下に例を示します。

    例:
    得られた値
      20536 6446 24555 213 9583 33333 24555
    

    counterの内部
     counter[213] = 1
     counter[6446] = 1
     counter[9583] = 1
     counter[20536] = 1
     counter[24555] = 2
     counter[33333] = 1
    

    ソート済み配列として扱う
      213 6466 9583 20536 24555 24555 33333
    

     この構造により、シグネチャを計算する為のskipsとkがアクセスする添字の位置のピッグデータを取得する事が出来ます。上記の例のデータを用いると、k=4の場合はcounter内の値を足し合わせて4を越えた時の値(>4)である24555が得られる値となります。このデータ構造の場合、各ピッグデータの元のindexデータは消失します。しかしシグネチャ計算においてはソート済みのピッグデータだけが必要となるので問題ないと判断しました。

    マルチスレッド化


     実行環境のPCはCPUのコアが4つあったので、getsignの計算をマルチスレッド化する事によって効率化を図りました。与えられた範囲のgetdataで得られた個数をカウントするCounterクラスを作成し、ExecutorServiceで任意の個数のスレッドを実行する様にしました。計算する値は1スレッド当たりcount/スレッド総数個とし、全てのスレッド計算が終わったあと、カウント用配列(65536個の配列)をマージする事でcount個のカウントデータが得られる様にしました。

    SHA-1がボトルネックとなる


     getdataのソート情報は省メモリで得られる様になりましたが、107374182400回SHA-1のハッシュ計算をするのは大きな負荷です。処理時間の殆どがSHA-1のハッシュ計算に使われてるので、この部分を最適化する事で高速化を図れると考えられます。処理概要にも記述した通り、getdataではSHA-1によるハッシュ計算のインプットとなるqがindexを10で除算した値である事から、index〜index+9の範囲でqは同じになります。そこでqが変化するまでハッシュ値を一時的に保存する事によってハッシュ値計算の回数を減らす様にしました。これによりSHA-1のハッシュ計算の回数が1/10となりました。前述した実行環境においては従来10時間の実行時間だったものが1時間まで短縮する事が出来ました。


    ソース


    package jp.dip.sys1.pigdata;
    
    import java.security.MessageDigest;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    public class Main {
        public final static int SIZE_OF_16 = 65536;
        public static int THREAD_COUNT = 1;
        static class Count {
            long[] mCount = new long[SIZE_OF_16];
    
            public long[] getCount() {
                return mCount;
            }
    
            public void merge(long[] src) {
                for (int i = 0; i < src.length; i++) {
                    mCount[i] += src[i];
                }
            }
        }
    
        static class Counter implements Callable<Count> {
            protected long mBegin;
            protected long mEnd;
            protected byte[] mDigest;
            protected long mPrevQ;
            protected MessageDigest mMessageDigest;
    
            public Counter(long begin, long end) {
                try {
                    mBegin = begin;
                    mEnd = end;
                    mPrevQ = -1;
                    mMessageDigest = MessageDigest.getInstance("SHA-1");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            private byte[] toSHA1(String source) {
                mMessageDigest.update(source.getBytes());
                return mMessageDigest.digest();
            }
    
            public int getdata(long index) {
                long q = index / 10;
                long r = index % 10;
                if (mPrevQ != q) {
                    mDigest = toSHA1(String.valueOf(q));
                }
                mPrevQ = q;
                int upper16 = (0xff & mDigest[(int) r * 2]) << 8;
                int lower16 = 0xff & mDigest[(int) r * 2 + 1];
                return upper16 + lower16;
            }
    
            @Override
            public Count call() throws Exception {
                Count count = new Count();
                long[] countData = count.getCount();
                for (long i = this.mBegin; i < this.mEnd; i++) {
                    int data = getdata(i);
                    countData[data]++;
                }
                return count;
            }
        }
    
        public static long getsign(final long count, final long skips) {
            if (count < 1 || skips < 1) {
                return -1;
            }
            int threadCount = THREAD_COUNT;
            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
            List<Future<Count>> futures = new ArrayList<Future<Count>>();
            for (int i = 0; i < threadCount; i++) {
                long begin = (count / threadCount) * i;
                long end = (count / threadCount) * (i + 1);
                if (i + 1 >= threadCount) {
                    end += count % threadCount;
                }
                futures.add(executor.submit(new Counter(begin, end)));
            }
            Count resultCount = new Count();
            for (Future<Count> future : futures) {
                try {
                    Count c = future.get();
                    resultCount.merge(c.getCount());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            long[] countData = resultCount.getCount();
            long result = 0;
            for (long k = 0; k < count; k += skips) {
                long index = 0;
                for (int l = 0; l < SIZE_OF_16; l++) {
                    index += countData[l];
                    if (index > k) {
                        result += l;
                        break;
                    }
                }
            }
            executor.shutdownNow();
            return result;
        }
    
        public static void main(String[] args) {
            long start = System.currentTimeMillis();
            System.out.println(getsign(107374182400L, 16777216L));
            System.out.println(System.currentTimeMillis() - start + "ms");
        }
    }
    

    おわりに


     おもろかった。

    2013年5月3日金曜日

    OnActivityForResultCallbackFragmentというモノを作ってみました

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
     どうもActivityだのFragmentだのの、startActivityForResult()とonActivityResult()の流れがすげーめんどいので何とかしたいと思いつつまぁいいかーと思っていたのだけど、やっぱだるいので何とかしようかなと。で、丁度Fragment触っていたのでFragmentを先にサポートしてみました。

    仕組み


     OnActivityForResultCallbackFragmentはFragmentを継承しており、内部にOnActivityResultListenerインタフェースを定義してます。メンバ変数にSparseArrayを持ち、startActivityForResult()の第三引数にOnActivityResultListenerを取る様にしています。渡されたOnActivityResultListenerをrequestCodeをキーとしてSparseArrayに保存し、onActivityResult()の時に取り出してコールバックを呼ぶ、だけです。詳しくは以下の実装を見て下さい。

    コード


     Gistです。上段のコードがOnActivityForResultCallbackFragmentです。下段は使い方です。OnActivityForResultCallbackFragmentを継承してstartActivityForResultにコールバック渡すだけです。これFragmentだけどextendsの所と名前をActivityにすればそのまま使えるはず。


    おわりに


     ありそうで無かったような。実はありそうな。とりあえず継承して使わないといけない点が気に入らないけど一応動くし、onActivityResult()を介す事で実装がバラけるのは防げるので、サクッとstartActivityForResult()したい時には便利なんだろうなぁと。

    2013年4月28日日曜日

    Strokesというストロークを再生出来るお絵描きWebアプリ的なのを作りました

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

    はじめに


     2日ほど前の夜中に@kojira@hamatzらが

    といった感じで語らっていたので、

     という事で作りました。ものはこちらです。HTML5的なWebアプリです。

    Strokes


    できる事


     大体以下の事が出来ます。メインはトレースモードでしょうか。書いた絵を1ストロークずつ再生し、それをなぞる感じです。 
  • 線画を書く
  • アンドゥ・リドゥ
  • 絵を保存する(localStorage)
  • 保存した絵の再編集
  • 保存した絵の削除
  • 書いた絵のストロークを再生
  • 絵のトレースモード


  • デモ


    詳しくは以下の動画を。絵描きさんの絵をトレースモードでなぞって練習とか色々捗るんじゃないかなと。



    おわりに


     まだ排他処理やってなかったりだとか、サーバへのデータ保存出来なかったりだとか、ペイントツール部分もしょぼかったりします。なんか要望とかアイデアあればStrokes Issuesに書いて下さい。あわよくばpull request下さい。あとUIデザイン周りと、コンテンツとして良さげな線画書いてくれる方募集。