ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

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年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年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年1月4日金曜日

Androidでjava1.7をどうしても使いたい場合はこうしたらええで

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
Androidでjava1.7は使えないらしい、けど、何とかする方法はありました。
結論からいうと別においしくない。

何故Androidでjava1.7が使えないか


javaをコンパイルして生成されるclassファイルの中には、「オレ、このバージョンでコンパイルされてるんでヨロ」という情報が入ってます。んでjavaの実行環境側でclassファイルをロードする時にサポートしているバージョンでコンパイルされたclassファイルかどうかをチェックして、もしサポート外ならエラーを投げる様になってます。

Androidだとclassを生成後、dexファイルに変換する所で以下の様に怒られます。
bad class file magic (cafebabe) or version (0033.0000)
「このきたならしい阿呆がァーッ!!」
classファイルのversionが間違ってると言われてますね。
0x33は51です。java1.7でコンパイルするとclassファイルのバージョンは51になるんすねー。1.6なら50。

で、どうする


dexで怒られるのでAndroidで使えないのは分かりました。じゃあどうしたらええのか。長いjavaの歴史、互換性に関しても色々ノウハウあるんちゃうん、という事でありました。下位互換のオプションが。

Java7シンタックスで レガシーコードを快適メンテナンス
とか
javac - Java プログラミング言語コンパイラ
を見ますと、javacに-target付けてコンパチなclassファイルが作れるようですね

Eclipse+ADTでは無理


結論から言うとEclipseでは無理です。
設定で以下の様に生成するclassファイルのバージョン指定とか出来ますが、



ADTに怒られます。

Android requires compiler compliance level 5.0 or 6.0. Found '1.7' instead. Please use Android Tools > Fix Project Properties.

無理です。

antならいける


antではどうでしょうか。

Android用のbuild.xmlを見る


とりあえずANDROID_HOME/tools/ant/build.xmlの-compileタスクを見てみます。このbuild.xmlはandroidコマンドなどで生成したAndroidプロジェクトに生成されるbuild.xmlでimportされているAndroidアプリケーションをビルドする為のタスクが詰まっているファイルです。
    <target name="-compile" depends="-build-setup, -pre-build, -code-gen, -pre-compile">
        <do-only-if-manifest-hasCode elseText="hasCode = false. Skipping...">
            <!-- merge the project's own classpath and the tested project's classpath -->
            <path id="project.javac.classpath">
                <path refid="project.all.jars.path" />
                <path refid="tested.project.classpath" />
            </path>
            <javac encoding="${java.encoding}"
                    source="${java.source}" target="${java.target}"
                    debug="true" extdirs="" includeantruntime="false"
                    destdir="${out.classes.absolute.dir}"
                    bootclasspathref="project.target.class.path"
                    verbose="${verbose}"
                    classpathref="project.javac.classpath"
                    fork="${need.javac.fork}">
                <src path="${source.absolute.dir}" />
                <src path="${gen.absolute.dir}" />
                <compilerarg line="${java.compilerargs}" />
            </javac>
~~略~~

javacタスクの所にtargetというのがありますね。
値は${java.target}という変数です。
${java.target}の定義を見ると
    <property name="java.target" value="1.5" />
    <property name="java.source" value="1.5" />

1.5になってますねー。
java.sourceの方はjavaファイルがどのバージョンかを表すオプション-sourceに渡す値です。こちらも1.5になっています。

これらを以下の様にするとどうでしょうか
    <property name="java.target" value="jsr14" />
    <property name="java.source" value="1.7" />

antでビルドしてテスト走らしてみる


上の設定にした状態で、以下のテストコードをBuild&Runしてみます。
ちなみにJUnit4 works on Androidあたりを参考にしてjunit4に対応させています。
    @Test
    public void Itemテーブルにレコードを1件追加する(){
        String moge = "ふがふが";
        switch(moge){
        case "ふがふが":
            Log.d("tag", "hogeeeeee!");
            break;
        case "moge":
            Log.d("tag", "moge");
            break;
        default:
            Log.d("tag", "あれえ");
        }
    }

はいビルド&実行
ant debug installd
ant test

キタ━━━━(゚∀゚)━━━━!!
I/System.out(13216): org.junit.runners.Suite
I/TestRunner(13216): started: Itemテーブルにレコードを1件追加する(jp.dip.sys1.yagi.amp.sample.dao.ItemDaoBaseTest)
D/tag     (13216): hogeeeeee!
I/TestRunner(13216): finished: Itemテーブルにレコードを1件追加する(jp.dip.sys1.yagi.amp.sample.dao.ItemDaoBaseTest)

いけましたね。

ANDROID_HOME/tools/ant/build.xmlをいじるのは嫌だよね


基本的にANDROID_HOME配下のヤツをいじって何かに対応するのは緊急の場合を除いてしたくないっすね。もろに環境依存だし。つーことで、なんとかANDROID_HOME/tools/ant/build.xmlのプロパティを外部からいじれねーかと、もちろんいじれます。
ant debug -Djava.target=jsr14 -Djava.source=1.7

こうです。-Dオプションです。debugの所はreleaseとかinstrumentでももちいけますです。これでjava.targetとかが書き換えられます。

終わりに


java1.7の機能でめぼしいのってtry-with-resourcesと例外のマルチキャッチくらいだなーと思ってるけどjsr14では

try-with-resourcesは使えないようなので

なんだかなーと。(try-with-resourcesはAutoCloseableというjava1.7で追加されたinterfaceで実現しているので当然1.6以前には存在しないから無理ぽという事。これ以外でもjava1.7で追加されたAPIとかはもち使えないと思う。)
あとEclipseで使えないので実用性はあんまりないなーと。
どーしても使いたい、という場合向けだけど微妙。

以上

2012年2月23日木曜日

[Android] アクションバーを画面下側から表示させてみました。アプリ編

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

@adakodaさんが面白い事をしていらっしゃったのでインスパイア。
[Android] アクションバーを画面下側から表示させてみました
http://www.adakoda.com/adakoda/2012/02/android-47.html

アプリからでも出来るのでは?


アプリケーションではアプリを初期化する際にsetContentView()するんで、setContentView()で出来たView群が画面上の全てだと思いがちです。しかーし画面上に存在する以上タイトルバーやActionBarなども当然Viewの一種なわけです。ちゅーことはアプリケーションのトップレベルのViewを取れたらタイトルバーやActionBarも操作出来るのでは無いでしょうか!?

そもそもまずトップレベルのViewをアプリケーション側から取得する事が出来るのか。出来ます。ActicvityからWindowを取り出してgetDecorView()するだけ。
View root = activity.getWindow().getDecorView()

ではこのViewどういう構造になっているか。Viewツリーをダンプしてみましょう。ViewツリーのダンプはViewの階層構造をダンプするスニペットを参考に。
View v = getWindow().getDecorView();
Util.dumpViewTree(v, "");

吐かれたのがこれ。Android4.0.2のGalaxy Nexusで、プロジェクトはEclipse+ADTで生成して出来たものを全く触らずに利用。ターゲットは4.0です。

Util  D  com.android.internal.policy.impl.PhoneWindow$DecorView
Util  D   android.widget.LinearLayout
Util  D    com.android.internal.widget.ActionBarContainer
Util  D     com.android.internal.widget.ActionBarView
Util  D      android.widget.LinearLayout
Util  D       android.widget.ImageView
Util  D       android.widget.LinearLayout
Util  D        android.widget.TextView
Util  D        android.widget.TextView
Util  D      com.android.internal.widget.ActionBarView$HomeView
Util  D       android.widget.ImageView
Util  D       android.widget.ImageView
Util  D     com.android.internal.widget.ActionBarContextView
Util  D    android.widget.FrameLayout
Util  D     android.widget.LinearLayout
Util  D      android.widget.TextView
Util  D    com.android.internal.widget.ActionBarContainer

"com.android.internal.widget.ActionBarContainer"てのが居ますね。露骨ですね。なんで二個あるのかはちょっとわかりません。

findViewsWithClass()を拡張


じゃあ"com.android.internal.widget.ActionBarContainer"を取り出して操作したらいいんじゃまいか?どうやって取り出すの。ああ、何かそういうエントリを昨日見たよ。あった。List<T> findViewsWithClass(View v, Class<T>)。助かるわー。#ステマ

しかしどうもList<T> findViewsWithClass(View v, Class<T>)ではClass<T>を使っています。com.android.internal.widget.ActionBarContainerは名前しか判らないのでちょっと拡張する必要がありそうですね。

こうなった
public static <T extends View> List<T> findViewsWithClass(View v, Class<T> clazz) {
  List<T> views = new ArrayList<T>();
  findViewsWithClass(v, clazz.getName(), views);
  return views;
 }
 public static List<View> findViewsWithClassName(View v, String className) {
  List<View> views = new ArrayList<View>();
  findViewsWithClass(v, className, views);
  return views;
 }
 private static <T extends View> void findViewsWithClass(View v, String clazz, List<T> views) {
  if (v.getClass().getName().equals(clazz)) {
   views.add((T) v);
  }
  if (v instanceof ViewGroup) {
   ViewGroup g = (ViewGroup) v;
   for (int i = 0; i < g.getChildCount(); i++) {
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

これでcom.android.internal.widget.ActionBarContainerを簡単に取り出せます。

View root = activity.getWindow().getDecorView();
List<View> views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");



試しにsetVisibility(View.GONE)してみる


では試しに取り出したcom.android.internal.widget.ActionBarContainerをView.GONEで消してみましょう。

View root = activity.getWindow().getDecorView();
List<View>l views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");
for(View v : views){
  v.setVisibility(View.GONE);
}

消えたー!操作出来る。当たり前だけど。


actionBarUpsideDown(Activity activity)


Viewツリーのダンプを見ると、事実上ルートのコンテナはcom.android.internal.policy.impl.PhoneWindow$DecorView直下のLinearLayout。多分Vertical。つまり、この人にいるcom.android.internal.widget.ActionBarContainerをremoveView()で消して、addView()で足すとケツに移動するのではないか!?
という事でメソッドを作成。

public final static void actionBarUpsideDown(Activity activity) {
  View root = activity.getWindow().getDecorView();
  View firstChild = ((ViewGroup) root).getChildAt(0);
  if (firstChild instanceof ViewGroup) {
   ViewGroup viewGroup = (ViewGroup) firstChild;
   List<View> views = Util.findViewsWithClassName(root, "com.android.internal.widget.ActionBarContainer");
   if (!views.isEmpty()) {
    for (View vv : views) {
     viewGroup.removeView(vv);
    }
    for (View vv : views) {
     viewGroup.addView(vv);
    }
   }
  }
  else{
   Log.e(TAG, "first child is not ViewGroup.");
  }
 }

試す


メソッドにしたので試すのは簡単ですね

setContentView(R.layout.main);
Util.actionBarUpsideDown(this); 

キタワァ.*・゜゚・*:.。..。.:*・゜(n‘∀‘)η゚・*:.。. .。.:*・゜゚・*!!!!!☆



まとめ


ICS以外で呼ぶとどうなるか試してないので適宜何とかして下さい。
ActionBarが上に居るのは個人的に良く思っていないので、これ使って下に表示させる様にしていきたい。



Viewの階層構造をダンプするスニペット

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
まぁViewっていうかViewGroupですけど。

コード


こうです。あんまり階層が深いとスタックオーバーフローで死ぬと思います。
public static void dumpViewTree(View v, String padding){
  Log.d(TAG, padding + v.getClass().getName());
  if(v instanceof ViewGroup){
   ViewGroup g = (ViewGroup)v;
   for(int i = 0; i < g.getChildCount(); i++){
    dumpViewTree(g.getChildAt(i), padding+" ");
   }
  }
 }

実行

試しにandroid.support.v4.app.ListFragmentのonCreateView()で自動的に生成されるViewをダンプしてみました。 こういう構造してたんですねー。
27152 Util  D  android.widget.FrameLayout
 27152 Util  D   android.widget.LinearLayout
 27152 Util  D    android.widget.ProgressBar
 27152 Util  D   android.widget.FrameLayout
 27152 Util  D    android.widget.TextView
 27152 Util  D    android.widget.ListView

まとめ


デバッグとか動的にView書き換えてる場合とかAPIが勝手に生成するViewについて調べるのに使えるのではないでしょうかー。





2012年2月22日水曜日

List<T> findViewsWithClass(View v, Class<T>)

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

android.view.ViewといえばfindViewById(int)ですよねー。
実はそれ以外にもfindViewWithTag (Object tag)やfindViewsWithText(ArrayList, CharSequence, int)てのがあるようです。
使い所がよくわかりませんがなんか使えそうですね。

個人的にはViewのクラス名で取り出したいなーと、例えばjavascriptのgetElementsByTagName(name)のノリだと言うと判ると思います。

List<T> findViewsWithClass(View v, Class<T>)


はい、完成。本当は v instanceof Tってやりたかったけど怒られました。仕方なくClassのフルネームで比較してます。なのでサブクラスとかまでは検出できません。

public static <T extends View> List<T> findViewsWithClass(View v, Class<T> clazz){
  List<T> views = new ArrayList<T>();
  findViewsWithClass(v, clazz, views);
  return views;
 }
 
 private static <T extends View> void findViewsWithClass(View v, Class<T> clazz, List<T> views){
  if(v.getClass().getName().equals(clazz.getName())){
   views.add((T)v);
  }
  if(v instanceof ViewGroup){
   ViewGroup g = (ViewGroup)v;
   for(int i = 0; i < g.getChildCount(); i++){
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

※追記20120223
@zaki50さんからツッコミが。ありがとうございます!!



直した!。これの場合だとサブクラスまでがっつり取れる。

 private static <T extends View> void findViewsWithClass(View v, Class<T> clazz, List<T> views) {
  if (clazz.isAssignableFrom(v.getClass())){
   views.add(clazz.cast(v));
  }
  if (v instanceof ViewGroup) {
   ViewGroup g = (ViewGroup) v;
   for (int i = 0; i < g.getChildCount(); i++) {
    findViewsWithClass(g.getChildAt(i), clazz, views);
   }
  }
 }

使う


こう使うとよさげ。例えばこういうレイアウト

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="mogeee" >
    </TextView>
    <ImageView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" >
    </ImageView>
    <LinearLayout
        android:id="@+id/main_list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
    </LinearLayout>
</LinearLayout>

こういうコードで要素を取り出せます。以下はTextViewを取り出してます。

List<TextView> views = findViewsWithClass(v, TextView.class);
        if(views.isEmpty()){
         Log.d(TAG, "not found");
        }
        else{
         Log.d(TAG, "find:"+views.size());
        }

2012年2月20日月曜日

サポートパッケージのFragmentでstartActivityForResultを使う場合の注意点

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

ついにFragmentに入門したわけですが、早速ハマったのでメモしておきます。サポートパッケージェ・・・。
※本エントリはandroid.support.v4.app.Fragmentに関する動作について書いています。android.app.Fragmentでは以下に書いている問題は発生しません。

FragmentでstartActivityForResultが使える


Fragmentって便利ですねー。特にActivityにベッタリ書いていた様な実装をFragmentに分けて色々捗らせる事が出来る点がとても助かります。で、Activityに書いていた処理をFragmentに集約する時に問題になるのがstartActivityForResultによって処理を委譲している部分です。

FragmentではstartActivityやstartActivityForResultやonActivityResultが用意されており、FragmentからActivityを起動したり、結果を受け取る事ができます。

という事でまぁ問題なくねーと思いがちですがしかし、FragmentのstartActivityForResultには罠があるんですねーーーー!


リクエストコードは下位16bitの範囲で


まず以下のコードをFragment内で実行してみます。なんの変哲もない、アドレス帳を起動して選択結果を受け取るだけのIntentです。

private final static int REQUEST_CODE = 0x11111;
public void pick() {
 Intent intent = new Intent(Intent.ACTION_PICK, 
  ContactsContract.Contacts.CONTENT_URI);
 startActivityForResult(intent, REQUEST_CODE);
}

し、死んだー!

Can only use lower 16 bits for requestCode


なにーー!
そうですFragmentではstartActivityForResultの第二引数に与えるリクエストコードを下位16bit以内に収める必要があります。
つまり0x0000〜0xffffまでの間の値にしろ、という事ですね。上記コードは"0x11111"だったのでダメだったんですね。

リクエストコードは下位16bitの範囲で。



onActivityResultが呼ばれないパターン


こういう罠にはまるのは僕くらいのものな気がしますが、サポートパッケージのFragmentのstartActivityForResultではonActivityResultが呼ばれないパターンがあります。
親となるActivity側がonActivityResultをオーバーライドしている場合に起きるのですが・・・。
コードを見てみます。問題無いようなあるような・・・。

親となるActivity側
@Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data){
  switch (requestCode) {
  case REQUEST_CODE:
   //なにもしない
   break;
  case MOGE:
   //~処理~
   break;
  }
 }
Fragment側
@Override
 public void onActivityResult(int requestCode, int resultCode, Intent data) {
  switch (requestCode) {
  case REQUEST_CODE:
   if(resultCode == Activity.RESULT_OK){
    Log.d(TAG, "okkkk!");
   }
   else{
    Log.d(TAG, "ooooops!");
   }
   break;
  }
 }

このコードの場合Fragment側のonActivityResultが呼ばれません。何故か?

super.onActivityResultを忘れているから!


そうです。親Activity側のsuper.onActivityResultを呼んでませんでした。
サポートパッケージでFragmentを使う場合はActivityではなくFragmentActivityを継承する必要があります。
このFragmentActivityのonActivityResultでFragmentに結果を渡しているのでsuper.onActivityResultを呼んであげないとFragment側にonActivityResultの呼び出しが行かないんですねー。

正しくは
@Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data){
  super.onActivityResult(requestCode, resultCode, data);
  switch (requestCode) {
  case REQUEST_CODE:
   //なにもしない
   break;
  case MOGE:
   //~処理~
   break;
  }
 }

問答無用でsuper.onActivityResult。



まとめ


コンパーチビリティの為のサポートパッケージですがなかなかどうして、大変ですねー。


2012年2月11日土曜日

【Android】スムーズな開閉アニメーションを実現するExpandAnimatorというライブラリを作りました

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

デモ


まずは動作を。




この様に指定したViewの開閉を行う為のアニメーションライブラリを作りました。


ダウンロード


Androidのライブラリプロジェクトをgithubに公開しています。git cloneして下さい。

ExpandAnimator
https://github.com/sys1yagi/ExpandAnimator

構成は以下の通りです。


動画のデモはsampleの中に入っているサンプルプロジェクトを動作させたものです。


サポートバージョン


1.6〜


使い方


ExpandAnimatorの使い方は簡単です。

ライブラリプロジェクトを参照する


まずExpandAnimatorをライブラリプロジェクトとして参照します。



トリガとコンテナをlayoutXMLに定義する


開閉する対象となるViewと、開閉のトリガとなるボタンなどをlayoutXMLに書いておきます。
ここではボタンがトリガで色のついたLinearLayout部分がコンテナです。


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

    <Button
        android:id="@+id/trigger"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="5" />

    <LinearLayout
        android:id="@+id/container"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="10dp" >

        <View
            android:layout_width="fill_parent"
            android:layout_height="300dp"
            android:background="#ffccddff" >
        </View>
    </LinearLayout>

</LinearLayout>


ExpandAnimatorを使う


開閉アニメーションを行う為にExpandAnimatorクラスを利用します。
ExpandAnimatorは開閉対象となるViewを内部に持ってアニメーション制御を行います。

下記コードの20行目でExpandAnimatorをnewしています。引数に開閉対象となるViewを渡しています。
第二引数は開閉アニメーション時に呼び出されるリスナを指定します。

41行目でアニメーションの時間を指定しています。
42行目ではアニメーションの為のインタポレータをセットしています。DecelerateInterpolatorはだんだんアニメーションのスピードが減速していくインタポレータです。

43行目でトリガとなるボタンにOnClickListnerをセットし、onClick(View)でExpandAnimatorのexpand(), unexpand()をそれぞれ呼び出しています。

package test.test.test.test.test;

import jp.dip.sys1.yagi.android.expandanimator.ExpandAnimator;
import jp.dip.sys1.yagi.android.expandanimator.ExpandAnimator.OnAnimationListener;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;

public class ExpandSampleActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Button trigger = (Button) findViewById(R.id.trigger);
        //ExpandAnimatorを初期化する
        final ExpandAnimator animator = new ExpandAnimator(findViewById(R.id.container), new OnAnimationListener() {
            @Override
            public void onUnexpanded(ExpandAnimator e) {
                trigger.setText("onUnexpanded");
            }

            @Override
            public void onStartUnexpand(ExpandAnimator e) {
                trigger.setText("onStartUnexpand");
            }

            @Override
            public void onStartExpand(ExpandAnimator e) {
                trigger.setText("onStartExpand");
            }

            @Override
            public void onExpanded(ExpandAnimator e) {
                trigger.setText("onExpanded");
            }
        });
        //アニメーション時間を指定
        animator.setDuration(1000);
        //インタポレータを指定
        animator.setInterpolator(new DecelerateInterpolator());
        
        //トリガを設定
        trigger.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (animator.isExpand()) {
                    //閉じる
                    animator.unexpand();
                } else {
                    //開く
                    animator.expand();
                }
            }
        });
    }
}


出来た


こ、こいつ動くぞ・・・!



使いどころ


わかりません。


ライセンス


Apache License 2.0です。


2012年1月31日火曜日

android.app.Dialogを任意のサイズに設定する。 FILL_PARENT, WRAP_CONTENTもできるよ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
さんざんハマったので。

こうだ


こうです。
dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);

android.app.DialogをnewしてレイアウトぶちこんだあとgetWindow()してsetLayoutでサイズ指定する感じです。
この時FILL_PARENT, WRAP_CONTENTなども使えます。

import android.widget.LinearLayout.LayoutParams;

Dialog dialog = new Dialog(context);
dialog.setContentView(R.layout.hoge);
dialog.setTitle(R.string.moge);
dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);




Dialogの横だけFILL_PARENTにする


まぁこうですね。

import android.widget.LinearLayout.LayoutParams;

Dialog dialog = new Dialog(context);
dialog.setContentView(R.layout.hoge);
dialog.setTitle(R.string.moge);
dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);




Dialogを300x300にしてみる


まぁこうですね。

import android.widget.LinearLayout.LayoutParams;

Dialog dialog = new Dialog(context);
dialog.setContentView(R.layout.hoge);
dialog.setTitle(R.string.moge);
dialog.getWindow().setLayout(300, 300);




使ったレイアウトXML


は、これ。android:textにリテラルぶっこむとLint先生に怒られるので良い子の皆様は必ずvaluesで定義してstringを下さい。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:padding="10dp" >
    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:text="(さよならだ)"
        android:textColor="#FFF" />
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >
        <Button 
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="OK"
        />    
    </LinearLayout>
</LinearLayout>



2011年12月26日月曜日

Androidアプリケーションのパーミッション改竄を検知するスニペット

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

はじめに



AppNetBlockerというINTERNETパーミッションを消しちゃうアプリが出ました。
そもそもアプリ改竄自体いいのかどうか。まぁ怪しいパーミッション消したいとかいうのも判るけども。
AppNetBlockerに限らず実際にはINTERNETパーミッションどころじゃなくこんなアプリ↓まで既にAndroid Marketでは販売されています。

https://market.android.com/details?id=com.gmail.exathink.appshield

一部でapk改竄は著作権の侵害に当たるといった声もありますねー。しらんけど。
僕はよくわからないんでとりあえずなんとも言えないですけど、

パーミッション改竄されたかどうかは検知しとくかー

という感じです。


実装


どっかのUtilクラスにでもコピペして下さい。

    private static List<String> missingPermissions(Context context, String[] expectations) {
        return missingPermissions(context, new ArrayList<String>(Arrays.asList(expectations)));
    }
    private static List<String> missingPermissions(Context context, List<String> expectations){
        if (context == null || expectations == null) {
            return null;
        }
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(getPackageName(), PackageManager.GET_PERMISSIONS);
            String[] info = pi.requestedPermissions;
            if (info != null) {
                for (String i : info) {
                    expectations.remove(i);
                }
            }
            return expectations;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

使い方


missingPermissionsにアプリケーションが本来持っているべきパーミッションのリストを渡すと、消えてる奴が返ってきます。
改竄されてなければ空リストが返るはずです。nullの時は多分何かエラーが発生していると思います。知らん。

import android.Manifest.permission;
//〜〜略〜〜
List<String> missing = missingPermissions(this, 
    new String[]{permission.INTERNET, permission.WRITE_EXTERNAL_STORAGE}
);
if(mission != null && !missing.isEmpty()){
    //なんか改竄されてる!
}

こういう風に使えるかもしれないですね



おわりに


Twitterを引き続き注視していきまーす。


2011年12月18日日曜日

え、まさか難読化の為にわざわざproguard.cfgをいじってるんですか?

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

photo by http://www.flickr.com/photos/jonnygoldstein/

裏Android Advent Calendar 2011 12/18(日)担当の@sys1yagiです。

いきなりですがproguard.cfgをいじるのってクソめんどくせーわけですよ。何が悲しくてあんな意味不明なオプションの文法を毎回ググりながらsitと言いながら書かないといけないのか。ていうか複数人でやってたらproguard.cfgをいじるタイミングがさー色々面倒でもう最後にぱっと追加するかーとか皆思っちゃってて最後にカオスになったりして\(^o^)/とかさ、もっと簡単な方法はねーのかよ!?例えばアノテーションでさー、クラスのとこに書いたらさー、難読化されないとかさ。

あるでー


はい、ProguardがそもそもAnnotation用のライブラリを提供しています。これを使えば難読化の対象外にしたいクラスに所定のアノテーションを書くだけで難読化から逃れられます。普通にProguardが提供している機能なので、全くトリッキーな事なしでいけます。ちょっと副作用がありますが。


導入の準備


ProguardはオープンソースでSoruceForgeで公開されています。
アノテーションによる難読化の回避はこのSourceForgeで配布されているアーカイブに含まれているライブラリを利用します。ダウンロードページからproguard4.6.zipをダウンロードして下さい(多分4.6じゃなくてもいけると思いますが)。
http://sourceforge.net/projects/proguard/files/



導入


proguard4.6.zipを展開し、proguard4.6/examples/annotations/lib/に入っているannotations.jar、annotations.proをAndroidプロジェクトにコピーして下さい。annotations.jarはビルドバスを通しておきます。今回はproject/libsに置きました。


するとアノテーションで@Keepとか書けるようになります。


次にプロジェクト直下あたりのproguard.cfgの先頭辺りに以下の二行を記述します
-dontshrink
-include libs/annotations.pro
annotations.proはproguard.cfgと拡張子は異なりますが中身は大体同じです難読化についてのオプションが記述されています。-dontshrinkは書かないと何故かアノテーションが消えるので書きます。すかさず-keepattributes **を思いついたあなたはやっぱり凄い!ですがこのオプション書いてもダメでした。四の五の言わずに書いて下さいませね。

これだけでおk


実行


あとはProguardにかけるだけ。そういやAntで直接Proguardやれば-dontshrinkはいらないかもしれません。ここではEclipseのAndroid Toolsで"Export Signed Application Package ..."とかやっています。
難読化実施後のクラス構成


見事@KeepをつけたDonotObfuscationクラスが難読化されていません。
メソッドはどうでしょう。javapしてみます。


@KeepNameのついたnotObfuscation()は難読化されていませんね。obfuscation()の方は難読化されている事がわかります。
Keep以外にも色々なアノテーションがあります。色々使えそうですね。


副作用


良い事ばかりじゃもちろんありません。ちょっと副作用がありますんで注意して下さい。


不要クラスが残る


-dontshrinkは利用していないクラスなどの削除を行なってバイナリサイズを小さくしてくれる処理を禁止します。つまりProguardにかけた対象となるクラスが難読化後全て残ります。物によってはapkサイズがでかくなってしまいます。ただ、ほぼ気にしなくてもいいレベルだと思います。精々数KBの違いでしょう。これなら他のリソースの容量削った方が効率的だと思います。


何を難読化したくないかが一覧できない


proguard.cfgにKeepオプションなどを書く場合は全部書かれているので一覧性がありますが、アノテーションの場合はソースに個別に書くのでどのクラスが難読化したくないかについて分かりにくくなります。まーKeepとかでソース検索すりゃいいしどーでもいい気もしますがー


そういえば


annotations.proの中身を見てみるとこんな感じです。

-keep @proguard.annotation.Keep class *

-keepclassmembers class * {
    @proguard.annotation.Keep *;
}

つまりふつーにProguardのKeepの条件に「指定のアノテーションがある場合難読化しねーぜ」、と設定してるだけです、つまり別にProguardの提供しているannotations.jarを使わなくても自分で適当にアノテーションを定義して、annotation.proみたいなのを書いてincludeするかProguard.cfgに書くだけでいけるわけです。つまりそういう事です。


結論


皆楽になるといいですねー。
メリークリスマスー
明日は@patorashと@Nkznさんです。わーい。

2011年12月16日金曜日

Honeycomb以降ではMotionEventのACTION_DOWNでevent.get(1)すると死ぬ

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

まさかこんな実装をしているわけがない、と思います。僕くらいです。はい。おおonTouchメソッドよ死んでしまうとは情けない。

こうすると死ぬ


常識的に考えて以下のコードは気持ち悪いですよね。でもAndroid3.0未満、つまりGingerbread以前では問題なく動作します。しかしHoneycomb以降ではjava.lang.IllegalArgumentExceptionが発生してしまいます。エラーメッセージはpointerIndex out of rangeです。なんででしょうね?!

@Override
public boolean onTouchEvent(MotionEvent event) {
 switch(event.getAction() & MotionEvent.ACTION_MASK){
 case MotionEvent.ACTION_DOWN:
  int x = event.getX(1);
  break;
 }
}


ソースを読む


MotionEventのソースを見ると・・・。げぇ全然違うぜぇえええ。

Android2.3.3_r1
public final float getX(
 int return mDataSamples[mLastDataSampleIndex
  + pointerIndex * NUM_SAMPLE_DATA + SAMPLE_X] + mXOffset;
}

Android4.0.1
public final float getX(int pointerIndex) {
        return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT);
    }


どう変わったか


上記の4.0.1のソースではもはやnativeメソッド。さすがに実装まで追うのはしんどいんで省略しますが、2.3.3_r1では配列アクセスをしている事がわかります。この配列はMotionEventのコンストラクタで初期化されます。配列サイズはコンストラクタに渡したpointerCount, sampleCountと定数NUM_SAMPLE_DATAを乗じた数となります。

private MotionEvent(int pointerCount, int sampleCount) {
 mPointerIdentifiers = new int[pointerCount];
 mDataSamples = new float[pointerCount * sampleCount * NUM_SAMPLE_DATA];
 mEventTimeNanoSamples = new long[sampleCount];
}

現タッチ数は内部でmNumPointersで管理しています。2.3.3_r1のgetX(int)では利用していません。つまりgetX(int)でアクセスするインデックスに影響を与えません。MotionEventはイベントを受け取るViewで使い回しているので(一応MotionEventのhasCode()で確認した)、コンストラクタで確保された配列をオーバーしない限りは死なない事になります。

操作 実タッチ数 Gingerbread以前 Honeycomb以降
event.getX(0) 2 ○ ○
event.getX(2) 2 ○ ×
event.getX(100000) 2 × ×


結論


今までギリギリ動いていたけどHoneycomb、ICSで爆死、そんな事があるかもしれませんね。怖いわー。




2011年11月14日月曜日

Activity#runOnUiThread(Runnable)の実装を読む&注意点

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
実はrunOnUiThreadを最近まで知らなくていつもアホみたいにHandler#postしていてなんだか悔しかったのでActivity#runOnUiThread(Runnable)の実装を読んでみました。

Activity#runOnUiThread


Activity#runOnUiThreadの実装がこちら。

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

内部的にHandler#postしていますね。
ここでちょっと気になるのが
Thread.currentThread() != mUiThread
この比較です。runOnUiThreadを実行しているスレッドがUIスレッドかどうかチェックしています。
そしてUIスレッドである場合は
action.run();
指定したRunnableをrunOnUiThreadの中で即実行しています。つまりrunOnUiThreadはRunnableを実行する為にブロッキングされるという事です。

どんな時問題になるか


runOnUiThreadをHandler#postと同じモノだと考えて利用しているとハマる可能性があります。
まぁ多分大丈夫と思いますが以下のhello2()様な実行順に依存性があるような実装をしちゃうとヤバイですね。

Handler mHandler = new Handler();
String mMessage = "hello!";
private void hello(){
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}
private void hello2(){
    runOnUiThread(new Runnable(){
        @Override
        public void run() {
            Toast.makeText(getApplicationContext(), mMessage, Toast.LENGTH_SHORT).show();
        }
    });
    mMessage = "good bye!";
}

別スレッドからhello(),hello2()、UIスレッドからhello(),hello2()を実行するとToastに何が表示されるでしょうか?

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "good bye!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。


結論


色々省略しますが
・Handler#post, runOnUiThreadに渡すRunnableが触る変数にはHandler#post, runOnUiThread呼び出し後触らない。
・Handler#post, runOnUiThreadはなるべくメソッドの最後に呼ぶ
・Handler#post, runOnUiThreadに渡すRunnableの中では呼び出し元のメンバ変数などを触らない。
Handler#post, runOnUiThreadを呼び出すメソッド内でfinalな変数を宣言しておいてそれにアクセスしたりする様にする。

とかやってるといいんじゃないでしょかー


追記



といったリプライが。確かに。(@atsushienoさんありがとうございました!)

実際hello(), hello2()のmMessage = "good bye!";の直前にThread.sleep(1000);を追加して実行してみるとどうなるか実験してみました。
結果は以下です

呼び出しパターン 結果
別スレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
UIスレッドからHandler#postを呼ぶ(hello()) "hello!"が表示される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) "good bye!"が表示される
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) "hello!"が表示される

なんか一個動きが違うのがいるーーーーー。
はいそういう事ですね。
以下の様な事になります。

呼び出しパターン 動き
別スレッドからHandler#postを呼ぶ(hello()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからHandler#postを呼ぶ(hello()) 必ず呼び出し後の処理が実行された後にRunnableが実行される
別スレッドからrunOnUiThreadを呼ぶ(hello2()) 呼び出し後の処理とRunnableの実行どちらが先かは判らない
UIスレッドからrunOnUiThreadを呼ぶ(hello2()) 必ずRunnableが実行された後に呼び出し後の処理が実行される


結論は変わらず。
どちらにせよHandler#post, runOnUiThreadを呼び出した後にHandler#post, runOnUiThread内部で利用されうる値などの書き換えはやばいよ、という事ですね。
多分誰もこういう事はしないとは思いますが・・・。多分。