読者です 読者をやめる 読者になる 読者になる

なら@はてなブログ

福岡で働くスマートフォンエンジニア(おっさん)のブログ。更新頻度がとにかく低いのが悩み。

Android(5.0以上) + OkHttpでWi-Fi接続中でもモバイルネットワークが使われる

Androidアプリを開発していて、接続しているネットワーク状態に応じて処理を切り替えるような実装をすることは多いと思います。

よくあるのが大容量コンテンツのダウンロード処理をWi-Fi接続中だけ行うとか、Wi-Fiかモバイルネットワークかで画像ファイルの解像度を切り替えたりとか。

今回、そのような実装をしていて気になる動きがあったのでメモ書きです。

具体的な事象はタイトルの通りですが、Android 5.0以上の端末でWi-Fi接続中にもかかわらず、OkHttpを使用した通信にモバイルネットワークが使用される場合がありました。

執筆時点で使用していたOkHttpのバージョンは3.6.0です。

問題発生時の状況

Javaコード上でネットワーク状態を取得すると、Wi-Fi接続中と判定される状態です。

ConnectivityManager cm 
        = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isWiFiConnected = activeNetwork != null &&
                      activeNetwork.isConnected() &&
                      activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;

Log.i(TAG, String.format("isWiFiConnected : %b", isWiFiConnected));    // true

しかし、サーバ側でログを見ていると、Wi-Fi接続中であってもアクセス元がキャリア網のグローバルIPアドレスになっていました。発生する条件としては、アプリ起動中にネットワークがモバイルネットワークからWi-Fiに切り替わった場合です。

原因

状況から推測することは難しくないと思いますが、モバイルネットワークのコネクションが生きていて、Wi-Fiに切り替わった後も継続して使用されていました。

原因はOkHttpClientで使用しているConnectionPoolでした。

ConnectionPool.java

Create a new connection pool with tuning parameters appropriate for a single-user application. The tuning parameters in this pool are subject to change in future OkHttp releases. Currently this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.

ConnectionPool3.6.0時点の実装では「5つのidle connectionを保持し、5分間使用しないとプールが破棄される」という実装になっています。 また、通常であれば、ネットワーク状態が切り替わったタイミングでコネクションが使用不可になるため、プールは破棄されて新たにSocketが作られるようになっていました。

ただ、Android 5.0からアプリケーションが複数のNetwork Interfaceを使用できるようになったため、Wi-Fiが接続されてもモバイルネットワーク用のNetwork Interfaceがそのまま接続可能な状態になっているようでした。そのため、アプリがフォアグラウンドで頻繁にAPIコールをする場合などは、5分間の未使用という条件が満たされないので、ConnectionPoolは破棄されず、モバイルネットワークのConnectionを維持したままネットワーク通信が行われていたと考えられます。

なお、Wi-Fiからモバイルネットワークに切り替わった場合は特に問題なく(モバイルネットワークに切り替わる=Wi-Fi用のNetwork Interfaceはコネクションロストしている)、Wi-Fiから別のWi-Fiに切り替わった場合も問題ありません(同じNetwork Interfaceを使っているので、元Wi-Fiのコネクションはロスト)。

対応したこと

ということで、以下のような対応をいれることで問題は回避できたのですが、もっといい方法があれば知りたいところです。

ConnectivityManager.CONNECTIVITY_ACTIONを受ける適当なReceiverを作成して

private OkHttpClient mOkHttpClient;
private boolean mIsWiFiConnected;

@Override
public void onReceive(Context context, Intent intent) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        return;
    }
    
    ConnectivityManager cm 
            = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);

    NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
    boolean isWiFiConnected = activeNetwork != null &&
                          activeNetwork.isConnected() &&
                          activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
    
    // Clear pooled connections on network state changed to Wi-Fi from others.
    if (isWiFiConnected && !mIsWiFiConnected) {
        mOkHttpClient.connectionPool().evictAll();
    }
    mIsWiFiConnected = isWiFiConnected;
}

余談ですが、Build.VERSION.SDK_INTを判定してOSバージョンに応じたネットワーク変更検知(NetworkCallbacksを使うやつ。LとMでも違うし、ちょっと面倒)するように実装したところ、一部の端末でコールバックが飛んでこないという不具合が発生したため、上記のようにレガシーな方法をとりました。

Android TVの審査に通らない場合のTips

先日、Android TVアプリを開発してTV用のPlay Storeに登録したのですが、審査で微妙にハマったことがあったのでメモ書き。 と言っても、近いうちにこのTipsは不要になるはずですが。

審査で以下の理由でNGになった場合で、ドキュメント見直してもどこが悪いのかわからない場合に確認したほうがいいことがあります(たぶん日本人限定)。

Your app uses hardware features (such as a touchscreen or camera) that are not available on TV. 
If your app can operate without the use of those features, you'll need to modify your app's manifest 
to indicate that your app doesn't require these features.

基本的にここを読んで必要な対処をするのですが、表示言語を日本語にすると現時点(2016/10/02)でまだ情報が古いままなので、ここに書かれている項目に対応しただけでは審査に通らない可能性があります。特にTVアプリとモバイルアプリを同一apkとしてStoreに登録している場合です。

Handling TV Hardware | Android Developers

f:id:narazoro:20161002200420p:plain

f:id:narazoro:20161002200457p:plain

こちらはすでにGoogleさんに報告済みなのでおそらく近日中に修正されると思いますが、ドキュメント通りに実装したのに審査が通らなくて理由がわからない場合は、いちど表示設定を英語にして確認してみるのもよいと思います。

某ネットショッピングのアレをアレするChrome拡張を作りました

はい。9ヶ月ぶりの更新です。生きてます。

昨年の12月にスマートフォンアプリエンジニアとして某Web企業に転職したのですが、転職いっぱつめでiOSアプリ作ってからはJavaScriptばっかりやってます。

これまでの経験で、JavaScriptだけはぜんぜんやってきてなかったので大変ありがたかったのですが、未経験者ひとりにフロント(HTML5/CSS3込み)作らせるのは無理あるだろw。けど、そういうの嫌いじゃない。結果的にフロントもある程度できるようになって、僕のエンジニアとしての幅は広がったし。ありがたいこってす。

 

・・・と、いいつつもなんとかサービスリリースできて、比較的堅調に推移していますが、素人が作ったコードたくさんあるので内情はいろいろ大変。

あ、ここ1ヶ月ほどひっさびさにサーバサイドのチューニングとかもしてます。ますます何でも屋の器用貧乏になっていきそうな。

 

というわけで本題。

JavaScriptいろいろやってて、社内ツールとしてChrome拡張なんかもちょいちょい作るようになったので、いろいろ生活を便利にしていこうかと。

 

今は福岡勤務の月2回くらい東京出張が入る生活で、某楽天トラベルなんかで出張手配するんですが、疲れてたり急いでたりするとうっかりML購読のチェックを外しそこなって、しかも購読解除の導線がわかりづらいから毎回ウキーーーーってなってます。

 

そこで作りました。

楽天トラベル(ついでに楽天市場も)の購入確認画面のML購読チェックをデフォルト未選択にしてくれるChrome拡張。

※ただし楽天側のDOM構成が変わったら知らん。

 

GitHubで公開してますのでソースも持ってってください。

つか、ソースっていうほどのものじゃないけどw。

https://github.com/kazuki-nara/ChromeExtensions

 

ここの ml_not_check.crx をお使いのChromeに入れると、あのうっとーしいチェックがデフォルト消えます。

 

たぶん。

 

iOSでURLエンコード済み文字列がデコードできない場合がある

今回、iOSアプリのURLエンコード関連でちょっと気になることがあったのでエントリー。

症状としては、表題のままです。以下のURLエンコード済み文字列に対して以下のメソッドでデコードを試みた場合、きちんとデコードできずにnilが返ることがある。

- (NSString *)stringByReplacingPercentEscapesUsingEncoding:(NSStringEncoding)encoding

ぐぐってみても意外と同じような事例が見つからなかったので、文字コード変えたりしながらいろいろ試してみました。エンコード済み文字列じーっと見たり。

で、試してみると、nilになる場合はかなり限定されていて、Shift_JISエンコードした場合によく起きるみたい。
で、調べてみた。

wikipedia:URLエンコード

結論から書くと、うまくデコードできないパターンって、Wikipediaの記事を引用すると「マルチバイト文字はバイト単位で変換する。Shift_JISの2バイト目など、バイトが非予約文字に対応するなら、その文字をそのまま使用しても良い」このパターンのときです。要するに、エンコードされた文字列が、きちんと"%+16進数2桁"のみで表現されていない場合にnilが返る模様。

対策としては、いちばん簡単なのは"エンコードするときに省略形式にしない"ってことなんですが、それだとそのへんで拾ってきたエンコード済み文字列が復帰できないので、デコード時にやるとすると、「"%+3桁文字列"が見つかったら、3桁目を個別にURLエンコードして"%+16進数2桁"に変換した後で、stringByReplacingPercentEscapesUsingEncoding: を呼ぶ」ってのが正着でしょうか。あまりかっこよくないですが。

※サンプルソース書いたら後で追記します。

 

AppStore Review メモ書き

久々のiOSアプリ開発もなんとかものづくりはできたんですが、AppStoreの審査がなかなか通らない。(・ω・)

審査出すのも1年半ぶりくらいで、いろいろと変わってたのでメモ書き。

 

Resolution Center

アプリがリジェクトされると、Manage Applicationsの中にこれができる。

ここでAppleのReview Teamと直接やりとりが可能。以前はいちいちメールにFollow-up番号つけてやりとりしてたので、これはリジェクトくらったときの言い訳がやりやすくなりましたね。

注意点:これはステータスがRejectedの時だけ使えるので、新しいBinaryをアップすると消えます。その場合は、Contact usから続きのやり取りをするか(そういうカテゴリが選択できます)、もしくは、新しくアップしたBinaryのReview Notesに続きを書けばいいらしい。僕は今回、Contact usから送りましたが、Appleからの返信内容を読んだ感じ、どうもReview Notesに書いた方が話が早いっぽいですよ?

 

Metadata Rejected

Binaryには問題なくて、iTunesConnectの設定関連の場合は、上記のResolution Centerでやり取りすることになるみたい。で、Resolution Centerが開いてる間(要は次のBinaryをアップしない間)は、Review Teamのレスポンスもかなり早いので、Metadataの修正だけですむなら再審査はかなり早く終りそう。

 

Expedited App Review

これは前からあった、「アプリ審査を早くやってもらう裏技」ですが、同一アカウントから何回もやってるとけっこう無下に断られます。

なので、本当に急いでるときの最後の手段くらいにとっておいたほうがいいかも。

 

水着NG

そっかー。アイコンにちょっと水着が写ってるだけでも17+にしないとリジェクトくらうかー。

2012/03/09 追記

17+にしてもダメでした。というかガイドラインに書いてた。

アイコンとスクリーンショットについては、すべての画像について4+のレーティングでも表示できるものでなければならない。

 

というかガイドラインができてた

https://developer.apple.com/jp/appstore/guidelines.html

前はなかったよね? ガイドラインないとわかんねーよ!とか言いながらいろいろ修正してた黒い記憶ががが。

 

 

とりあえず開設してみました

ふと気がつくとはてなブログがオープンBETAになってたのでさっそく開設。つか、数時間前にはてダに記事投稿しちゃったよ。

お仕事がスマフォアプリの開発なので、ちょっとハマったことなんかをTips的に書いて行く予定。

 

はてダからデータ移行できるようになったら過去の記事もこっちに持ってきます。

過去記事(はてダ)

http://d.hatena.ne.jp/narazoro/

 

AppStoreの審査中にIn App Purchaseのデータが変になった件

さて、前の更新が2011年7月だったので、ちょうど半年ぶりの更新。
なにやったかというと、まあ転職してバタバタしておりました。現在は渋谷に拠点を構える緑色のWeb系企業でスマフォアプリの開発をしています。


それはさておき、今回も軽いTips程度の記事なのですが、自分でぐぐっても日本語の情報が出てこなかったので、同じ状況になった人の一助になれば。
前もって書いておきますが、たぶんApple公式ドキュメントのどっかに書いてるレベルだと思います。あしからず。


AppStoreに新規でアプリを登録しました。で、今回はIn App Purchase(以下、IAP)を使ってアプリ内課金を実装したので、IAPの商品もいっしょに登録して審査にだしました。
で、しばらくしたら審査が始まってリジェクトされたのですが、その頃からアプリ内のIAPの表示がおかしくなってまして。
具体的には、IAP商品の商品名とか説明がnullで戻ってきていました。また、購入確認ダイアログも「Unknown App」と表示されるようになって、まあリジェクトされたから一時的にそうなってるんだろうと思ってたんですが、そのあとでiTunesConnectにバイナリアップして再審査に出してもそのままでした。


これ原因は、「アプリがリジェクトされたタイミングで、一緒に登録したIAPの商品もリジェクトされたから」なんですが、ちょっとだけハマったのは、「最初にアプリのバージョンとIAP商品の関連を設定したんだから、バイナリアップしたらIAPも一緒に再審査になるだろう」と思ってたところ。
IAP商品と紐づいたアプリバージョンがリジェクトされた場合、その後でアプリのバイナリアップしようがメタデータを修正しようが、IAP商品自体のステータスはRejectedのままです。で、その状態だと上記のようにIAP商品の情報が取得できないままになります。(そのまま審査に出しても、それが原因で再度リジェクトされると思われます)


これを解消するには、Manage In App Purchaseで商品をそれぞれ修正して、ステータスを再びWaiting For Reviewに戻してやる必要があります。
今回は6商品登録してたので、ひとつひとつ修正してステータスを戻しました。めんどくさい・・・。
で、IAP商品にはなにも問題ない場合でも、アプリがリジェクトされるとIAPもまとめてRejectedにされます。そういう場合は、内容を修正する必要はないのですが、「ちょっとでいいのでどこか変更しないとステータスがRejectedのまま」なので、適当に文字列かえたりしてください。
ちなみに、いったん変更かければRejectedじゃなくなるので、その後でもとに戻すのはアリです。


そんな感じでー。
転職前はAndroidばっかりやってたので、ひさびさにiOSアプリ作ると手続きがめんどいですねえ・・・。