Drowsy Dog's Diary

any note, any thought

2014年9月24日
by kazoo
0 comments

既存アプリの iOS8 / XCode6 対応でつまづいたところ色々

例によってドラスティックに変化する iOS 新バージョン。
8対応で引っ掛かったところと、その参考リンクのメモです。

精確さは保証しませんので、ツッコミいただけると有り難し。。

本家リファレンス

What’s New in iOS 8
https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS8.html

API 7.1 -> 8.0 Diffs
https://developer.apple.com/library/ios/releasenotes/General/iOS80APIDiffs/index.html#//apple_ref/doc/uid/TP40014455

Trait Collection

What’s New in iOS 8 # New Screen Size and Scales

[iOS 8] マルチデバイス対応の新機能「Trait Collection」| Developers.io

画面サイズについて、Size Class という考え方が加わり、

定義はこんな感じ:

UITraitCollection というクラスを用いてデバイス種別(iPhone/iPad)やタテヨコの Size Class (Compact / Regular)をそれぞれ取得するようになった。
従来の UIDevice クラスの UIUserIntefaceIdiomUIInterfaceOrientationを使わず(Deprecatedではないみたい)、デバイスや向きに関係無い「長いor短い」という定義が加わった感じだろうか(要するに iPhone6+ のためね)。

#これ、「Class」だけどオブジェクト指向的な意味じゃなくて Size Class という概念を導入する、ということなんですよね。ややこしい。 Group とか Category(これも ObjC 的にはだめか)とかって名前にすればよかったのに。

また、iOS8 において、UIView の width/height は、Portrait/Landscape それぞれの状態に応じたタテヨコのサイズを返すようになっている。

http://stackoverflow.com/questions/24150359/is-uiscreen-mainscreen-bounds-size-becoming-orientation-dependent-in-ios8

[[UIScreen mainScreen] bounds].size.width
[[UIScreen mainScreen] bounds].size.height

は、
iOS7 の 4 inch iPhone では方向によらず(320, 568)だが、
iOS8 では Portrait時 (320, 568)、Landscape時 (568, 320) となる。
Viewのフルスクリーン指定・判定など、方向に拠らないコードになっている場合は注意が必要。

ちなみに、iPhone 6 / 6+ の解像度の考え方については、以下のページがどちらもわかりやすい。
http://www.paintcodeapp.com/news/iphone-6-screens-demystified
http://www.dotproof.jp/2014/09/11/iphone-6-ppi/

Display Scale

さらに、iPhone6+ の UIScreen は、これまでの Retina ディスプレイが2倍だったのに対し、論理ピクセルから3倍のスケーリング(1242 x 2208) を行い、さらに15%のダウンスケーリングをして 1080 x 1920 ディスプレイに表示している。

OpenGL ES(または Metal)を使うアプリではこれらを気にせず、GLKView の contentScaleFactor プロパティを、UIScreen の nativeScale として取得できるものに設定してやれば良い。

…らしいが、今のところうまくいかない(2/3 で表示される)。何故だ。。

UIViewのアニメーション

http://stackoverflow.com/questions/24472663/ios-8-animation-bug

これはバグなのか仕様なのかよくわかってないのですが(上記StackOverflowではバグだろって言ってるけどたぶん違う)、たとえば UIView をフルスクリーン化するとき、

といった感じでアニメーションしてくれていたコードが動かなくなった。
frame の代わりに、

と、bounds と center を使うとうまくいった。

Launch Images の作成

http://qiita.com/uebo/items/854c284ed11aca741d93

iPhone6+ 用として、@3x の画像が必要になってきます。。とりあえず @2x があればそっちを使ってくれる。

Signing のエラー

http://qiita.com/eggmobile/items/fff1e80c51ae03f3f471

“Your account already has a valid iOS Distribution certificate”

というエラーの対処。
Xcode 6 への更新後に起こる模様。

Provisioning Profile をリネームして作り直せとのこと。Apple の iOS Dev Center に行って、当該のプロファイルを Edit し、名前だけちょっと変えて再度 Generate → DL すれば OK。

Submit / Validation のエラー

http://stackoverflow.com/questions/25806661/app-validation-on-xcode-6-gm-release
http://prod.lists.apple.com/archives/xcode-users/2014/Sep/msg00034.html

修正が終わって、いざ submit しようと思ったら、

ERROR ITMS-4088: “No software found with apple_id: xxxxxxxxx” at Software Assets

というエラー。xxxxxxxxx は、アプリの Apple ID で、もちろん iTunes 上に存在する。
そして Validation を行うと、

iTunes Store operation failed.
No eligible software found. Make sure this software is owned by your developer organization.

Apple 内でも調査中らしい。
とりあえず、Organizer でなく、現状 Application Loader を使えとのこと。
まずは .ipa ファイルを Export しておいてから、
XCode > Open Developer Tool > Application Loader
で、Distribution 用アカウントでログインしてアップロードしてやれば OK。

2014年8月25日
by kazoo
0 comments

[android] APK Expansion Files(2)

前回の続き。Expansion Files のダウンロードについて。

アプリの起動時、あるべき Expansion files がそのパス(shared storage location の Android/obb/<package-name>/
)になかったらその時点で Google Play から DL してくる必要がある。アプリは Application Licensing service を使って対応するファイル名・サイズ・URL を取得し、ファイルを DL して適切なパスに保存を行わなければいけない。

前記事の通り、このための Downloader Library とそれを使ったサンプルが SDK に用意されている。また、Google Play にライセンスの Verification を行い URL をリクエストする Licensing サービスは以下に用意されている。

<ANDROID_SDK_DIR>/extras/google/play_licensing/

ルールと制限

  • Expansion File の上限はそれぞれ2GB
  • ユーザは Google Play からアプリを DL していなければならない。それ以外の方法でインストールされたアプリは Expansion File を DL できない
  • Google Play が提供する URL は、すべての DL に対してユニークであり、短時間で無効化される
  • Expansion Files の更新には、APK 本体の更新も必要
  • obb/ ディレクトリに他のデータを保存してはいけない
  • .obb ファイルを削除・リネームしてはいけない

Expansion Files のダウンロード

Licensing Verification Library (LVL)での認証処理に加えて、ダウンロードのための HTTP 接続とファイル保存のコードが必要になる。またそのために考慮すべき Issue は、

  • ストレージ容量が足りているかの確認
  • UI をブロックせず、またダウンロード途中でもユーザが離脱できるようにバックグラウンドで動作すること
  • ダウンロード中に起こる各種エラーへの対応
  • ネットワークの切断に対応し、また可能であれば途中からレジュームする
  • バックグラウンドでのダウンロード中、そのステータスと進捗をユーザに知らせること

Google 製の Downloader Library を使うことで、これらのタスクをすべて簡素化できる。開発者は、以下のライブラリをアプリに組込めば利用可能。

  • <ANDROID_SDK_DIR>/extras/google/play_licensing/
  • <ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_library/

Downloader Library が Licensing Library に依存する形になっているので、Eclipse などのプロジェクトに組込んで、Main となるプロジェクトのプロパティで前者のライブラリプロジェクトを追加してやればよいはず。

また、zip フォーマットを Expansion Files として使う場合は、APK Expansion Zip Library の使用も推奨されている。

パーミッション

Expansion Files のダウンロードには、Manifest に以下のパーミッションの宣言が必要。

ダウンローダの実装

Downloader Library は DownloaderService という Service のサブクラスを提供する。開発者はこれを継承して自前のダウンローダを実装する。DownloadService はまた、

  • BroadcastReceiver を登録し、必要なときにダウンロードを停止・再開するためにネットワーク接続状況を監視する
  • サービスが kill されたときのためにリトライ用のタイマーをスケジュールする
  • ダウンロード進捗あるいはエラーを表示するための Notification を持つ
  • マニュアルでダウンロードが pause/resume できる仕組みを与える
  • shared storage がマウントされ利用可能であること、ファイルがすでに存在しないこと、容量が足りることを確認して NG の場合は知らせてくれる

開発者は、DownloadService を継承したクラスを作り、以下の3つのメソッドを Override する。

ここで、BASE64_PUBLIC_KEY は、Developer Console で確認できる各開発者アカウントに対応する鍵に変更する。
また、getSALT() は、SharedPreferences に保存される License のデータを難読化するためのソルト値。これも別のランダムな値に更新しておく。
getAlarmReceiverClassName() は、ダウンロードが(何らかのエラー後に)再開したときのアラームを受け取るアプリ内の BroadcastReceiver を返すようにしておく。

追加したサービスは忘れずに Manifest にも追記しておく。

AlarmReceiver の実装

ダウンロードの進捗をモニターし、必要に応じて再開するために、DownloaderService は RTC_WAKEUP アラームを設定する。開発者は BroadcastReceiver を Downloader Library から呼べるように定義する。

ダウンロードの開始

前回記事に書いた Helper ライブラリを使ってExpansion Files の存在(無いこと)を確認したら、下記の static メソッドを呼んでダウンロードを開始する。

DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, Class serviceClass).

パラメータの意味はそれぞれ:

  • context: アプリケーションの Context
  • notificationClient: メイン Activity を開始するための PendingIntent。DownloaderService からの Notification で使用され、ダウンロードの進捗を表示する(通常はDL開始したものと同じ)Activity を開始するための PendingIntent を登録する
  • serviceClass: DownloaderService の Class オブジェクト。サービスを開始しダウンロードを始めるために必要

戻り値は以下のようになる。

  • NO_DOWNLOAD_REQUIRED: ダウンロード不要
  • LVL_CHECK_REQUIRED: Expansion Files の URL を得るためライセンスのベリフィケーションが必要
  • DOWNLOAD_REQUIRED: すでに URL が判明しておりダウンロードが開始可能

通常は LVL_CHECK_REQUIRED と DOWNLOAD_REQUIRED の違いを意識しない。Main の Activity から startDownloadServiceIfRequired() を呼び、単純に戻り値が NO_DOWNLOAD_REQUIRED かどうかをチェックすればよい。 NO_DOWNLOAD_REQUIRED 以外の何かが返ったときは、Downloader Library は ダウンロードを開始し、その進捗を表示するための Activity をセットアップする。

サンプルは以下の通り。

startDownloadServiceIfRequired() が NO_DOWNLOAD_REQUIRED 以外を返したときは、DownloaderClientMarshaller.CreateStub(IDownloaderClient client, Class downloaderService) を呼んで IStub インスタンスを生成する。IStub は Activity と DownloaderService をバインドする(進捗の通知などのため)仕組みを提供する。CreateStub() には、IDownloaderClient インターフェイスを実装した DownloadService の実装を渡す。

この IStub の生成は Activity の onCreate() で startDownloadServiceIfRequired() の後に行うことが推奨される。

先ほどのサンプルの続きは以下のようになる。

onCreate() を抜けた後、onResume() の中で IStub の connect() を呼ぶ。そして onStop() で disconnect() を呼んでやるとよい。

進捗・エラーステートの受信

Downloader Library にある、IDownloaderClient インターフェイスを使う。通常は、ダウンロードを開始した Activity にこれを実装する。

onServiceConnected(Messenger m) は、IStub を生成した後 DownloaderService との接続を通知してくれる。これを受けたら、pause/resume が送れるように DownloaderServiceMarshaller.CreateProxy() を呼んで、IDownloaderService のインターフェイスを取得しておく。

また、onDownloadStateChanged(int newState), onDownloadProgress(DownloadProgressInfo progress) で、それぞれ状態や進捗の変化を通知してくれる。

STATE_COMPLETED が返れば ダウンロード完了。必要に応じてダウンロードしたファイルのバリデーションなどを行い、Activity を抜ければよい(これも Downloader Sample にサンプルがある)。また、IDownloadService を取得した後は、それに対して requestPauseDownload()/requestContinueDownload() を呼ぶことで、手動で中断・再開ができる。

Expansion Files のテスト

Google Play に APK と Expansion Files を UP する前に、まず正しいパスから Expansion Files を読み出せるかをテストする。
たとえば、パッケージ名が com.example.android であれば、Shared Storage に Android/obb/com.example.android/ ディレクトリを作成し、$ adb push などでファイルを設置し、Google Play 上で扱われる正しいファイル名に .obb ファイルをリネームする。
ファイル名フォーマットは、main.0300110.com.example.android.obb のようになる。バージョンコードは何でもよいが、アプリがそれを正しく知っていることが必要。LVL は Expansion Files ではなく、あくまでも APK のバージョンコードに基づいて、Google Play 上のリソースを探しにゆく。ここでバージョンコードが正しくないと、”Download failed because the resources could not be found” というエラーになるので注意。

ダウンロードのテストは、Google Play の Beta バージョンとして Expansion Files をアップロードすれば、通常の APK 同様にベータテストユーザがダウンロードしてテストすることができる。

2014年8月22日
by kazoo
1 Comment

[android] APK Expansion Files

Play Store に登録する Android アプリのサイズ上限は 50MB。
これを超えるサイズのアプリを登録したい場合は、APK Expansion Files という仕組みを使って、別ファイルにアプリを分割することで、最大 4GB までのアプリを登録できる。
こないだ超えてしまったのでこれについて調べた。

先に結論言うとかなりめんどいので、できるだけやらない方がよいと思う。

最近のゲームでよくあるように、小さな APK で起動して、自前のサーバと自前のダウンローダで必要なデータを都度取ってくる仕組みが用意できるなら、もちろん必要はない。

拡張ファイルについて

2種類ある。main と patch。それぞれ最大 2GB。
patch 拡張ファイルはオプション的に、main 拡張ファイルに対して小さな更新を担うイメージ。使うのは main だけでもよい。

それぞれ独立して Google Play に登録でき、APK から使用する拡張ファイルのバージョンを指定できるが、「拡張ファイルだけ」を更新することはできない。拡張ファイルを更新するときは、APK(のバージョンコード)の更新も必要。

拡張ファイルのフォーマットは ZIP, PDF, MP4, etc.何でも可能。また、jobb というコマンドツールが用意されており、これを使ってファイルセットを .obb というひとつの暗号化ディスクイメージのファイルにまとめることができる。obb は Opaque Binary Blob の略。

jobb コマンドは下記にあり、

<ANDROID_SDK_DIR>/tools/jobb

という感じで使う。

  • -pn: アプリのパッケージ名
  • -pv: パッケージのバージョン。ここで指定したものより大きなバージョン(android:versionCode)のアプリが、これをマウントできる
  • -d: 入力ディレクトリ名。これで指定したディレクトリ以下のファイルとサブディレクトリがひとつの obb にまとめられる。-d resource としたとき、解凍すると resource ディレクトリそのものは無いので注意
  • -k: obb を暗号化する場合のパスワードを設定
  • -o: 出力ファイル。名前はなんでもよい

Google Play に拡張ファイルをアップロードすると、ファイルタイプに関係無く以下のフォーマットにリネームされる。

[main|patch].<expansion-version>.<package-name>.obb

上記の例だと、main.4.com.hogehoge.pkgname.obb となる。

拡張ファイルのダウンロード先とマウント

以下に、ダウンローダのサンプルとヘルパーライブラリが用意されている。ファイルパスの取得などはヘルパーを使うのが楽。

<ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_sample/
<ANDROID_SDK_DIR>/extras/google/play_apk_expansion/downloader_library/

ダウンロードされた拡張ファイルは、
<shared-storage>/Android/obb/<package-name>/

以下に保存される。
<shared-storage> は、getExternalStorageDirectory() で取得できるパス。一般的には /sdcard になると思う。<package-name> は前述のパッケージ名と同じ。

アプリは自分が必要な拡張ファイルのバージョンを知っているので、まずこの obb ファイルを探しに行き、StorageManager を使ってこれをマウントする。ざっくり書くとこんな感じ。

マウントに成功すると、onObbStateChange(String, int)が、/mnt 以下のそのパスを教えてくれる。

ダウンロードの保証

ほとんどの場合、拡張ファイルはアプリ本体と一緒にダウンロードされる。ユーザからは Google Play 上の(50MB より大きな)ひとつのアプリとしか見えない。

しかし、まれに拡張ファイルのみダウンロードに失敗したり、ユーザが手動でファイルを削除したりする場合がある。このケースについてもアプリは最初に拡張ファイルの存在を確認し、無い場合には自前でダウンロードできるのが望ましい。

…が、実際 50MB をちょっと超えるくらいであれば、そうそうダウンロードに失敗することは無いのではと思われる。ストレージが足りないときに起こるのかもしれないが、手元では一度も確認できなかった。ここは割り切って拡張ファイルが無ければエラー終了でも良いのではとも思われる(公式ドキュメントにはもちろん must be able to と書かれているが)。

で、先述のダウンローダライブラリと、

<ANDROID_SDK_DIR>/extras/google/play_licensing/library/

に用意されているライセンス用ライブラリを import して、Google Play から拡張ファイルのダウンロードを行うことができる。この方法も先のサンプルにほぼ書かれていますが力尽きたので後略。。。気が向けばまた書きます。

参考:
APK Expansion File | Android Developers
APK拡張ファイルの使用 | Android デベロッパー ヘルプ

Expansion Files について | キノコの自省録

2014年8月21日
by kazoo
0 comments

[ios][appstore] In-App Purchase プロダクトとアプリバージョンの紐付け

これもやるたびにミスって Developer Reject するので今さらだけどメモっておく。。

iTunesConnect に新規のアプリ内購入(IAP)アイテムを追加するときの順序について。

基本的にここでは Non-consumable プロダクトについて書いてます(これしかやったことない)。
間違い等ありましたらご指摘いただけると重畳。

現バージョンのアプリでそのまま購入できるアイテムを追加する場合

これは単純に、アプリ管理画面の “Manage In-App Purchase” から “Create New” ボタンで追加して submit すればよい。

ProductID や値段など、基本的な情報を登録して Save すれば、 Submit する前にテストアカウントで購入テストができるようになる。この時点でサムネイルはまだ無くてもOK。

テストアカウントは、iTunesConnect トップの “Manage Users” から登録できる。Android と違って「購入キャンセル」というものが(事実上)存在しないので、同じアカウントで同じアイテムの購入テストは一度しかできない。なのでメールアドレスがいくつも必要になったりするが、そんなときは「10分間だけ有効な」アドレスを生成してくれる 10 Minute mail がべんり。

http://10minutemail.com/

テスト用購入確認ダイアログには、[Environment: Sandbox] という表記が付いている。レビュー用のサムネイルにどのような画像を使うのが適切か定かではないが、kazoo はいつもこの購入ダイアログのキャプチャをサムネイルにしている。

アプリのバージョンアップと同時に課金アイテムを追加する場合

問題はこのケース。未確認だがたぶん新規アプリの場合も同じ。

アプリのバージョンアップで買えるようになる課金アイテムは、まず上と同様に “Manage In-App Purchase”から新規アイテムを追加し、サムネイルを登録して “Ready to submit” の状態までもっていく。Submit はしない

次に、”Add Version” ボタンからアプリの新規バージョンを用意して、更新情報を追加する。ここで注意するのは、“Ready to Upload” ボタンを押す前に、このバージョンに課金アイテムを紐づける必要があること。状態が “Waiting for Upload” になってしまうと、もうそのバージョンは一度リジェクトしない限り紐付けはできない。

状態としては “Prepare for Upload”(または Developer Rejected)のときに、アプリの Deteil を見ると、このときのみ In-App Purchase の編集ができるようになっているので、これをエディットして、先ほどのアイテムを紐づければよい。Submit はまとめて行われるので、”Manage In-App Purchase” での Submit は必要ない。

IAP

アイテムを “Ready to submit” にする → アプリが “Prepare for Upload” のときにそれを紐づける。

書き記すと大したことではないのだが、うっかり失敗すると、一度 XCode から Submit して Developer Reject して(しかも次に再アップロードする際にまったく同じバイナリでは受け付けてくれない)というのが大変めんどくさい。アップロードとキャンセルが同時に走るのを避けたいのだろうが、Waiting for Upload を一回押してしまうと絶対にキャンセルできないのどうなん??と毎回思う。。

ちなみに、アプリバージョンと課金アイテムバージョンに依存関係がないからといって、In-App Purchase が Waiting for Review のときに次のアプリバージョンを Submit したりすると、まだレビュー通ってないアイテムに対して「このバージョンでこれ買えないぞ」みたいな Reject をされることがある(ぽい)。
基本的に、アイテムとバージョンを完全に紐づけるか、アイテムがレビュー中のときにはアプリ本体は触らない方が良さそう。

参考:
In-App Purhcaseプログラミングガイド

2014年7月2日
by kazoo
0 comments

[android]ギャラリーから画像を縮小して読込む

BitmapFactory でそのまま読むとメモリ不足が起きるようなでかい画像の場合、BitmapFactory.Options の inJustDecodeBounds を true にして、メモリに展開せずにパラメータだけをデコードできるので、その後あらためてスケールサイズ(inSampleSize)を指定してデコードしてやればよい。