Support Library v7 の ActionBarActivity を Android 4.1以下のLG端末で使うときの注意

Feb 23, 2015

概要

LG端末でだけ謎のクラッシュレポートが届いていたので調べた結果をメモ。 結論としては、LGの4.1以下端末に向けた回避コードが必要orz

なお、手元には該当の端末が無いため未検証。

クラッシュレポート

こんなレポートが送られてくる。

java.lang.NullPointerException
    at com.android.internal.policy.impl.PhoneWindow.onKeyUpPanel(PhoneWindow.java:987)
    at com.android.internal.policy.impl.PhoneWindow.onKeyUp(PhoneWindow.java:1686)
    at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:2097)
    at android.view.ViewRootImpl.deliverKeyEventPostIme(ViewRootImpl.java:3618)
    at android.view.ViewRootImpl.handleImeFinishedEvent(ViewRootImpl.java:3588)
    at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:2834)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4849)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:511)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:795)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:562)
    at dalvik.system.NativeStart.main(Native Method)

うん、自分の書いたコードに関する部分は全くないね。。。

原因と回避策

回避策はこのスレッドで議論されてた。 原因はLGカスタマイズAndroidのバグとのこと(参考死ねばいいのに

onKeyUponKeyDownをオーバーライドしてLG端末かつAPIレベル16以下で判定して回避処理を入れる。

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
     if ((keyCode == KeyEvent.KEYCODE_MENU) &&
	      (Build.VERSION.SDK_INT <= 16) &&
	      (Build.MANUFACTURER.compareTo("LGE") == 0)) {
	   return true;
    }
    return super.onKeyDown(keyCode, event);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    if ((keyCode == KeyEvent.KEYCODE_MENU) &&
	         (Build.VERSION.SDK_INT <= 16) &&
	         (Build.MANUFACTURER.compareTo("LGE") == 0)) {
	   openOptionsMenu();
     return true;
    }
    return super.onKeyUp(keyCode, event);
}

雑感

ただこれ、クラッシュレポート見てもどのActivityで起きてるかわからんのよね。 全部のActivityに入れるしか無いのか…

FLAG_ACTIVITY_REORDER_TO_FRONTで画面遷移するときのアニメーションを変更する

Oct 22, 2014

アクティビティを遷移する際のアニメーションを変更する場合は、startActivityの後にoverridePendingTransitionを呼ぶのが普通だと思いますが、インテントのフラグにFLAG_ACTIVITY_REORDER_TO_FRONTが設定されていると指定したアニメーションが適用されません。

Intent intent = new Intent(this, SampleActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent);
getActivity().overridePendingTransition(0, 0);

解決策はいつものようにStack Overflowで見つけましたが、備忘録として記録しておきます。
android - overridePendingTransition does not work when flag_activity_reorder_to_front is used - Stack Overflow

解決方法

FLAG_ACTIVITY_REORDER_TO_FRONTで既存のアクティビティを前面に移動させる場合には、呼び出された側のアクティビティのonNewIntentoverridePendingTransitionを実行する。

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    // FLAG_ACTIVITY_REORDER_TO_FRONTでActivityを起動した場合に
    // 起動元のoverridePendingTransitionが有効に機能しないため
    // インテントで起動時に呼び出すようにする
    overridePendingTransition(R.anim.transition_to_right, R.anim.transition_to_left);
}

onResumeで実行しても同じ動きになるけど、onResumeはいろいろなタイミングで呼ばれるのでおすすめしないよとのこと。

onNewIntentで実行すれば、インテントの内容でアニメーションを制御することもできるのでこちらでやるべきでしょう。

Volley + oauth-signpostでPOSTリクエストを署名する

Oct 15, 2014

(2015/02/09更新) フラグの持ち方に問題があったため完成版を修正

VolleyでTumblrにOAuthリクエスト

自作Tumblrクライアントアプリの通信周りは、oauth-signpostを使った自前実装で行っていました。
しかし、エラーハンドリングの容易さの魅力と、単純に使ってみたいと思ったという理由から実装をVolleyに書き換えるための調査を行いました。
やはり需要はあるようで、HurlStackを拡張してOAuthの署名を行う先人の知恵を幾つか見つけることができました。

しかし、これらのHurlStack拡張はGETリクエストに対しては有効に動作しますが、この方法でPOSTリクエストを行うと401が帰ってきてしまいます。

E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog

問題

POSTリクエストの場合、署名する前にconsumerにPOSTパラメータ等を渡してあげる必要がありますが、前述の例では開いたコネクションにすぐ署名しているためパラメータが足りていない状態です。

そこで、さらに前述のHurlStack拡張を修正していこうということになるのですが、これが意外に苦労しました。

まず、POSTリクエストを署名するにあたって、リクエストのパラメータが必要になるのですが、前述のHurlStack拡張が署名を行っているcreateConnectionメソッドではVolleyのリクエストを参照できません。

では、他のメソッドはどうかと確認すると、そもそもOverride可能なのはcreateConnectionのほかにはperformRequestだけです。

そして、performRequestにはVolleyリクエストが引数で渡されています。

なので、この2つのメソッドをOverrideしてPOSTリクエストを署名する方法を考えていきます。

試行錯誤

HurlStack.java - platform/frameworks/volley - Git at Google

HurlStack.javaを読んでみると、performRequestの中からcreateConnectionを呼ぶことでコネクションを生成しているようです。

なので、Volleyリクエストが引数として渡されているperformRequest内でOAuth用のパラメータをconsumerに設定する方法を試してみます。

なお、protectedなRequest#getParams()にアクセスするため、自パッケージ内のRequest拡張にキャストしています。

public class OAuthPostHurlStack extends HurlStack {

    private final OAuthConsumer mConsumer;

    public OAuthPostHurlStack(OAuthConsumer consumer) {
        mConsumer = consumer;
    }

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {

        if (request.getMethod() == Request.Method.POST) {
            if (request instanceof CustomRequest) {
                // POSTパラメータをHttpParametersに詰め替える
                HttpParameters oauthParams = new HttpParameters();
                Map<String, String> params = ((CustomRequest) request).getParams();
                for (String key : params.keySet()) {
                    oauthParams.put(key, OAuth.percentEncode(params.get(key)));
                }
                // POSTパラメータに加えてエンドポイントのURLも必要
                oauthParams.put("realm", request.getUrl());
                // consumerのパラメータとして設定
                mConsumer.setAdditionalParameters(oauthParams);
            }
        }

        return super.performRequest(request, additionalHeaders);
    }

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        // 最初に紹介した先人のものと一緒なので省略
        return connection;
    }
}

これでうまくいくのでは?と思い実行してみるもまたも401エラー…なにかが足りない…?

E/Volley﹕ [46076] BasicNetwork.performRequest: Unexpected response code 401 for http://api.tumblr.com/v2/blog/hogehoge.tumblr.com/post/reblog

解決

改めて、自前の実装とHurlStack.javaを見比べて不明な設定が無いか確認してみると、自前の実装では署名の前に、HttpURLConnectionのsetRequestMethod("POST")setDoOutput(true)を呼んでいました。

元のHurlStack.javaでは、当然ですがこの処理はcreateConnectionより後ろで行われています。

この際、HurlStackを拡張するのではなく、元から書き換えてしまおうかと一瞬頭をよぎりましたが、この2つの処理は本来の処理の前に1回実行してしまっても問題ないだろうと判断し署名前に設定することにしました。

public class OAuthPostHurlStack extends HurlStack {

    private final OAuthConsumer mConsumer;
    private ArrayList<String> mOauthSignedPosts = new ArrayList<>();

    public OAuthPostHurlStack(OAuthConsumer consumer) {
        mConsumer = consumer;
    }

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {

        if (request.getMethod() == Request.Method.POST) {
            if (request instanceof CustomRequest) {
                // OAuthで署名すべきPOSTリクエストのURLを格納する
                mOauthSignedPosts.add(request.getUrl());
                // POSTパラメータをHttpParametersに詰め替える
                HttpParameters oauthParams = new HttpParameters();
                Map<String, String> params = ((CustomRequest) request).getParams();
                for (String key : params.keySet()) {
                    oauthParams.put(key, OAuth.percentEncode(params.get(key)));
                }
                // POSTパラメータに加えてエンドポイントのURLも必要
                oauthParams.put("realm", request.getUrl());
                // consumerのパラメータとして設定
                mConsumer.setAdditionalParameters(oauthParams);
            }
        }

        return super.performRequest(request, additionalHeaders);
    }

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        try {
            if (mOauthSignedPosts.contains(url)) {
                // POSTリクエストのコネクションは署名前に設定が必要
                connection.setRequestMethod("POST");
                connection.setDoOutput(true);
                mOauthSignedPosts.remove(url)
            }
            mConsumer.sign(connection);
        } catch (OAuthMessageSignerException e) {
            e.printStackTrace();
        } catch (OAuthExpectationFailedException e) {
            e.printStackTrace();
        } catch (OAuthCommunicationException e) {
            e.printStackTrace();
        }
        return connection;
    }
}

これで無事、POSTリクエストも署名することができました。

まとめ

oauth-signpostでHttpURLConnectionのPOSTリクエストを署名するときは、署名の前に下記の2点が必要

  1. POSTパラメータ+エンドポイントのURLをパラメータとしてconsumerに設定する
  2. HttpURLConnectionのsetRequestMethod("POST")setDoOutput(true)を設定する

これ、一回自分でoauth-signpost使った実装してたから分かったけど、やってなかったら途中で挫折しただろうな…


今回作ったものはGistsにも上げているので、不具合問題あればプルリク大歓迎です。
OAuth signed POST request with Volley + oauth-signpost

自作のTumblrクライアントもGoogle Playで公開しているのでよろしくお願いします。
Android app on Google Play

任意の画像をインジケータとして使えるViewPager用タブ

Sep 23, 2014

ViewPagerを使用している時にActionBarで用意されているタブを使用せずにViewPagerのタブを作る必要がありました。 加えて、インジケータに画像を使用したいということもあり、既存のライブラリでぴったりはまるものが見つからなかったので自作してみました。
実装方法はこちら(JakeWharton/Android-ViewPagerIndicator)を大いに参考にさせてもらいました。

できたもの

Layoutの任意に位置に配置して使用できるViewPager用のタブウィジェット。インジケータには任意の画像を指定。

screenshot1

このスクリーンショットだけだと、ActionBarのタブでいいじゃないかって話になりますが、Layoutの中に自分で配置できるので画面の下部にタブを配置したり

screenshot2

ActionBarとタブの間に別のViewを差し込むことも可能です。

screenshot3

使い方

1. githubにライブラリプロジェクトとして登録しているので、cloneするなりZIPダウンロードするなりしてご自身のプロジェクトから参照できるようにしてください。(chibatching/ImageIndicatorTab

2. ActivtyやFragmentのレイアウトXMLの中に下記のように記述してViewを配置します。

	<com.chibatching.imgindicatortab.ImgIndicatorTab
    	xmlns:imgtab="http://schemas.android.com/apk/res-auto"
    	android:id="@+id/indicatorTab"
    	android:layout_width="match_parent"
    	android:layout_height="50dp"
    	android:background="#ffffff"
    	imgtab:selectedTextColor="#3333aa"
    	imgtab:deselectedTextColor="#888888"
    	imgtab:indicatorDrawable="@drawable/rect"
    	imgtab:fitIndicatorWithTabWidth="true" />

3. ActivityのonCreateやFragmentのonCreateViewで、AdapterのセットされているViewPagerをセットします。

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

    	// Create the adapter that will return a fragment for each of the three
    	// primary sections of the activity.
    	mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    	// Set up the ViewPager with the sections adapter.
    	mViewPager = (ViewPager) findViewById(R.id.pager);
    	mViewPager.setAdapter(mSectionsPagerAdapter);

    	// Set up the ImgIndicatorTab with ViewPager
    	ImgIndicatorTab imgIndicatorTab = (ImgIndicatorTab) findViewById(R.id.indicatorTab);
    	imgIndicatorTab.setViewPager(mViewPager);
	}

これで、任意の画像をインジケータに使ったViewPager用のタブが表示されます。
レイアウトXML内のパラメータの意味は下記の通り。

パラメータ名 意味
selectedTextColor 選択されているタブ名の文字色
deselectedTextColor 選択されていないタブ名の文字色
indicatorDrawable インジケータに使用するDrawable
fitIndicatorWithTabWidth  インジケータをタブ幅に合わせて拡大する(タブ高を超えない範囲で)

これから

とりあえず使えるようにと作ってみたので、今後時間を見つけて改良していきたい。
今考えているのは

  • インジケータ画像の配置をカスタマイズできるようにする(上下左右寄せ等)
  • インジケータ画像のスケールオプションを追加する(現状は幅合わせ or 何もしない)
  • 文字列がタブ幅より大きくなる場合の処理を追加する
  • エラー処理をちゃんと作る

Ubuntu Server 14.04でOpenVPNサーバを構築する

Sep 8, 2014

OpenVPNのインストール

aptからOpenVPN関連パッケージのインストール

sudo apt-get install openvpn libssl-dev openssl easy-rsa

証明書の作成

証明書作成用のディレクトリを作成

sudo make-cadir /etc/openvpn/easy-rsa

作成したディレクトリはrootが所有者なのでsuしておく

sudo su -

以下作業はrootで行う。 作成したディレクトリに移動

cd /etc/openvpn/easy-rsa/

CAファイル生成の設定を変更する

vi vars

ファイルの下の方にある該当箇所を修正

export KEY_COUNTRY="JP"	# 国
export KEY_PROVINCE="Kanagawa"	# 県
export KEY_CITY="Yokohama"	# 市
export KEY_ORG="hogehoge"	# 組織
export KEY_EMAIL="mail@hogehoge.com"	# メールアドレス
export KEY_OU="HOGE"	# 組織単位

鍵を作成する

source vars
./clean-all
./build-dh
./pkitool --initca
./pkitool --server server

cd keys/
openvpn --genkey --secret ta.key

作成した鍵をOpenVPNのディレクトリにコピーする

cp server.crt server.key ca.crt dh1024.pem ta.key /etc/openvpn/

server.confの編集

サンプルファイルが用意されているのでコピー・展開する

cd
sudo cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf.gz .
gzip -d server.conf.gz
sudo cp server.conf /etc/openvpn/

このセクションのこれから下はserver.confに対する変更になります。

以下の行を設定ファイルに追加する

mode server
tls-server

上記の方法で鍵を作成するとdh1024.pemではなく、dh2048.pemが作成されているので設定ファイル内のファイル名を変更

;dh dh1024.pem
dh dh2048.pem

自分のネットワーク環境に合わせて、OpenVPNクライアントに伝える情報を編集&行の有効化

push "route 192.168.xxx.0 255.255.255.0"

クライアントのデフォルトゲートウェイをVPNサーバにするために次の行を有効化

push "redirect-gateway def1 bypass-dhcp"

DHCP周りも自分の環境に合わせて設定する

;push "dhcp-option DNS 208.67.222.222"
;push "dhcp-option DNS 208.67.220.220"

push "dhcp-option DNS 192.168.xxx.yyy"
push "dhcp-option DOMAIN chibatching.com"

クライアント間の通信を有効にする

client-to-client

TLS暗号化を有効にする

tls-auth ta.key 0

暗号化の設定(クライアントとサーバで方式を合わせる)

cipher AES-128-CBC

セキュリティが向上するらしいので、OpenVPNの実行権限を下げる

user nobody
group nogroup

OpenVPNの起動確認

server.confの編集が終わったらOpenVPNの再起動

sudo service openvpn restart

 * Stopping virtual private network daemon(s)...     *   No VPN is running.
 * Starting virtual private network daemon(s)...     *   Autostarting VPN 'server'

Ufwでファイアウォールの設定

OpenVPNに通信を許可する

sudo ufw allow openvpn

/etc/ufw/sysctrl.confの以下の行を有効化

net/ipv4/ip_forward=1

こちらの記事を参考に/etc/ufw/before.rulesを編集する
OpenVPN with UFW | Nattee Niparnan

似たような記載のあるあたりに下記を追加

-A ufw-before-input -i tun+ -j ACCEPT
-A ufw-before-output -i tun+ -j ACCEPT
-A ufw-before-forward -s 10.8.0.0/24 -j ACCEPT
-A ufw-before-forward -d 10.8.0.0/24 -j ACCEPT

ファイルの最初の方に下記を追加

# rules for NAT Table of iptables
# required line for ufw
*nat
:POSTROUTING ACCEPT [0:0]

# Forward traffic from OpenVPN through eth0.
-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

# tell ufw to process the lines
COMMIT

まとめ

これで、OpenVPNクライアントからアクセスするための環境が整いました。 ここでは触れていませんが、当然ルータの設定も必要になってくるので、アクセスできない場合はそちらも確認してみてください。

クライアント側の設定や、クライアント用の証明書・鍵の生成についてはまた次の機会に…


Author

chibatching

chibatching