なるようになるかも

力は多くの場合、その人の思いを超えない。

強制バージョンアップの話。

という記事を見かけたので。

このライブラリの実装の問題点

key description
type 基本的には強制バージョンアップを行うことを前提に解説していますが、SRGVersionUpdaterではキャンセルボタン付きの告知アラートを表示することも出来ます。強制アップデートの場合は"force" を任意でのアップデートの場合は"optional"を入力して下さい。

これ、設計ミスってません?

一度致命的なバグを出して"force"で通知したら、それ以降二度と"optional"は使えません。「必ず一定以上のバージョンを使って欲しいけど、最新版の通知もしたい」ようなユースケースに対応できないなら、"optional"の存在意義はない気がします。

あと、「評価が付くまで様子見ユーザー」層は毎回"optional"の通知を繰り返し見せられて離れます。開発者がいいと思ったアップデートが必ずしもユーザーに受け入れられるとは限らない(むしろ逆のケースのほうが多い)のです…。

もう一点指摘するなら、一度強制バージョンアップ対象になった端末は、その後通信する必要がありません。何度起動してもストアに飛ばされるだけなので。

条件を付けてレスポンスをキャッシュすることで、余計な通信負荷を減らすことができます。機内モードに入れるなどして、バージョンチェックを迂回する裏道もなくなります。ただし サーバーの設定が間違っていた 場合の救済策がなくなります。

解決すべき問題について

古いバージョンのアプリで特定の操作を行うと、データが破損しアプリの操作が不可能となるバグが発見された

この本質的な原因は、単なるテスト不足です。

強制的に最新のバージョンに上げてしまうと、新たなバージョンに致命的なバグがあったとき、全ユーザーに被害が拡大するだけです。

品質管理の問題に対して、「強制バージョンアップの運用で解決する」という方法論を取るのは、正直微妙な感じがします。

サーバーサイドとの通信部分で使う認証ロジックに問題があり、古いバージョンでの認証ロジックのままでは成り済ましが出来てしまう脆弱性が発見された

これも根本的なところではテスト不足なのですが、それに加えてアプリとの連携APIを作る場合は、

  • 利用不可(非推奨)バージョンであることを通知する
  • 利用非推奨APIとなったことを通知する
  • サービスの終了(移行)を通知する

というのを、設計段階で想定するべきだと考えています。

これはリクエスト時にバージョンを受け取って、何らかのレスポンスコードで判定を行えばいいだけなのでまったく複雑ではないです。

アプリとAPIの寿命は長いとは言えません。設計段階で死の概念を内包しなければならない、と考えています。

楽ではないJSONのデプロイ

「たった3行」で導入できますが、運用は大変です。

これまでの経験上、最新版のアプリを配信開始しiTunes Connectの状態がReady for Saleとなっていても実際にApp Storeに反映されるまでには1〜3時間程度の時間差がありました。

にもありますが、アプリをデプロイしても実際に反映されるまでのラグを考慮する必要があります。全てのユーザーが本当にダウンロード可能になったのか、定期的に確認する必要があります。

加えて、サーバー側のデータの更新作業はユーザーが多い時間にやるべきではありません。

これはアプリの規模感にもよります。もしJSONに不備があっても、すぐに直して挽回可能な規模であれば当てはまりませんが、基本的には深夜層に実施することになるでしょう。深夜まで待機して、ミスったら大クレーム…、そんな作業やりたくない…。

バグのないプログラムがないように、必ず一度はヒューマンエラーが発生します。例えば、iOSAndroidを並行して開発した場合、両者のバージョンは基本的に一致しません。入れ違えただけで大惨事です。

古い端末のユーザーの考慮

AppStore固有の問題として、最新のバージョンとして「iOS7以降でしか動かないアプリ」をデプロイした場合、iOS6未満のユーザーは古いAPKを取得できてしまう というものがあります。

参考: アップル、古い iOSデバイス向けに互換性のある旧アプリを提供 - Engadget Japanese

一度アップロードしたバイナリはAppleによって管理され、開発者は制御できません。闇です。

UPDATE:訂正。アプリケーションのバージョンをiCloudに表示しない(iCloud availability)の設定で、旧バージョンが新規にダウンロードされることは防げる、が正しいです。

個人の感想

  • バージョンチェックは必須、ただし付けるなら認証なり各APIごとにきっちりやるべき
  • 全てのユーザーを強制アップグレードさせて、最新のAPIしか使わせないという選択肢は魅力的だけど、歴史を積み重ねたアプリだと破綻する可能性が高い
  • 「最新=完璧」という前提でいるのは危険
  • 簡単かどうかは実装ではなく運用で決めた方が良い

今更AndroidのToolbarについて理解できたような気がするのでメモ。

気のせいかもしれません。

Toolbar

Toolbar は Action Bar を一般化させたもので、同様の機能を備えつつ、より高い柔軟性を提供します。通常の Action Bar と異なる点は、Toolbar は階層内のビューの 1 つであるということです。そのため、Toolbar のインスタンスは好きなところに配置することができ、他のビューと共存します。さらに、動きを与えたり、スクロール イベントに反応させたりもできます。Activity.setActionBar() をコールすることで、Toolbar を Activity の Action Bar として機能させることもできます。

Android アプリにマテリアル デザインを導入する - Google Developer Japan Blog

これがいまいち意味分からなかったので、原文読んでみました。

Toolbar

The toolbar is a generalization of the action bar pattern, providing similar functionality, but much more flexibility. Unlike the standard action bar, toolbar is a view in your hierarchy just like any other, so you can place instances wherever you like, interleave them with the rest of your views, animate, react to scroll events and so on. You can make the Toolbar act as your Activity’s Action Bar by calling Activity.setActionBar().

Implementing Material Design in Your Android app | Android Developers Blog

generalization of the action bar patternを「Action Bar を一般化」は全然意味違ってないです?「ActionBarパターンを普遍化させた」とかそんな感じのニュアンスに読めます。

toolbar is a view in your hierarchy just like any otherの部分、「他のビューと同じ階層にある」という点についてちょっと曖昧な書き方です。タイトルバー階層と、アプリが管理しているビュー階層が今までは別だったという前提が重要だと思いますので。

interleave them with the rest of your viewsを「他のビューと共存」は意訳なのか自分の訳が悪いのか微妙ですけど、「他のビューの余白に挿入」ってだけに読めます。

ActionBarはビュー階層に解放されました

この記事がとても参考になりました。

~2.XからAndroidにはActivity単位でラベルを表示できるタイトルバーの機能がありました。標準の見た目が残念すぎて、requestWindowFeature()でカスタマイズができるにも関わらず、大抵非表示にされていました。

3.Xでタイトルバーは多機能なActionBarに置き換えられました。しかし「2.Xでタイトルバー非表示にしつつ、3.X~ではActionBarを表示」と分岐を入れるのは好まれず、またActionBarをバックポートできるのはActionBarSherlockという非公式ライブラリしかなかったので、依然非表示にされたまま、ActionBarパターンの認知度はいまいちでした。

その状況が変わったのが2013年7月のAndroid Support Libraryrevision18によるActionBarのバックポートです。時期的には4.3(Jelly Beanの最後のマイナーアップデート)になって、ようやく公式でActionBarがバックポートされ、「iOSとは違うAndroidのパターンを見直そう」という機運が高まります。

そして5.0になるわけですが、ここにきてActionBarの定義が大きく変わったように思います。ロゴ画像ではなくカラーを利用したブランディング、ユーザーの管理するビュー階層への移動、ActionBar組み込みのナビゲーション機能(setNavigationMode())の廃止。

もはやActionBarはユーザービュー階層にあるのですから、その実装はユーザー次第であると考えると、NavigationModeが廃止されたのも理解できます。

しかしタイトルバーを非表示にしてToolbarを使うというのは、直感に反していて非常に微妙…。

2つあるToolbar

Toolbarは2つあります。android.widget.Toolbarandroid.v7.widget.Toolbarです。

Toolbar は AppCompat で完全にサポートされ、フレームワーク ウィジェットと同等の機能と API をもちます。

AppCompat v21 — Lollipop 搭載前のデバイスにマテリアル デザインを! - Google Developer Japan Blog

これ、前提としてandroid.widget.Toolbarがあって、その同等の機能がandroid.v7.widget.Toolbarで提供されている、という意味なのですね…。実際使ってみるまでは、v7ライブラリ専用の機能だとばかり。

両者は実機能としては違いがないのですが、本家がandroid:Theme.Materialテーマを前提としているのに対して、v7版はTheme.AppCompatテーマを前提としており、属性参照している値が異なります。

例によって、android:toolbarStyletoolbarStyleに分裂してるんでしょう。

Toolbarを使うときの注意

setSupportActionbar()を呼んで、ActionBarとして使うときは、以下のようにレイアウト指定します。

<android.support.v7.widget.Toolbar
    android:id="@+id/my_awesome_toolbar"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary" />

そうではなく、普通にViewGroupとして使うときには指定が変わります。

<android.support.v7.widget.Toolbar  
   android:layoutheight="wrapcontent"
   android:layoutwidth="matchparent"
   android:minHeight="?attr/actionBarSize"
   app:theme="@style/ThemeOverlay.AppCompat.ActionBar" /> 

画面に複数Toolbarを表示する場合に、それぞれに異なるスタイルを指定するためなのだと思いますが、トラップ感満載です。

ところで単純なViewGroupとしてみた場合、layout_gravityがレイアウトエディタ上でサジェストされなくて使い勝手が悪いんですけど、バグなんでしょうねー…。

どうでもいいカレンダーの話。

iOSをやってるとあるあるなのが、ユーザーがOSの端末設定を「和暦」に設定してるせいで、取得されるカレンダーが日本のものになってしまい、NSDateFormatterが期待通りに動作しないというもの。

海外のアプリは和暦を考慮してないので上手くいけばクラッシュさせることができます。

西暦

現在普及しているのは西暦です。

個人的に、西暦という呼び方は物凄い雑だと思います。一般にグレゴリオ暦を指しますが、グレゴリオ暦そのものは暦法なのに対して、和暦は紀年法なので、暦法紀年法の区別が付いていないケースが見られます。

1年の長さを定義するのが暦法で、今が何年なのか紀元を定義するのが紀年法です。

単に西暦と呼んだ場合、グレゴリオ暦という暦法と、イエス生誕の翌年を西暦元年とする紀年法両方を指す気がします。

Before Christ(キリスト生誕前)は英語なのに、Anno Domini(主の年)はラテン語という、この良く分からない紀年法は1500年くらい前に考案されるも実際に普及したのは19世紀で、西洋諸国の植民地支配によって世界標準な地位を築きました。

また、実際的にユリウス暦が使われるケースがあることにも注意です。例えば天文学で使われる「1光年」が指すのは、グレゴリオ暦の1年ではなくユリウス暦の1年なのです。

仏暦

仏教徒を中心に、釈迦が入滅した翌年を元年とする仏暦が利用されています。

歴史は浅く、100年くらい前にタイで考案され使われ始めました。

タイは意地でもこの暦を使う気なのか、公文書などにも使いやがります。しかもタイ数字です。読めません。

Wikipediaには太陽暦とのずれがあると書いてあるけどこれは間違ってる気がする?今は普通に543で算出できたような…。

OSの表記などで「タイ仏暦」とあるのは、国によって釈迦入滅の年の解釈、つまり紀元年が違うためです。本当は複数仏暦があるのですがそのうち利用頻度の高い「タイ仏暦」のみ扱われることが多いです。

和暦

日本では1500年ほど元号による紀年法を使い続けています。また、使われていた暦法は以下の通りです。

  • 元嘉暦
  • 儀鳳暦

日本書紀で使われていたとされる暦です。

  • 大衍暦
  • 宣明暦

このころまでは中国で使われていた暦をそのまま日本でも利用していました。日本では暦学や天体観測技術が発達していなかったという事情があります。

  • 貞享暦

ここで初めて国内独自の暦法が採用されました。この辺の話は天地明察が面白いので読むべきです。

あの話だと授時暦の扱いはよくないけど、授時暦の観測精度そのものは、ユリウス暦を超える部分もありました。

  • 宝暦暦

そんな貞享暦は、800年以上続いた宣明暦とは裏腹に、たった70年しか持ちませんでした。

貞享暦による改暦は幕府と朝廷の間の軋轢を生んで、天文方の政治力がなくなったところで、再び土御門家が席捲、この残念な暦を制定してしまったとかどうとか。

天地明察は安井知哲の次男を天文方3代にしたところでハッピーエンドみたいな終わり方してますけど、その後の渋川家の顛末はなんとも切ない。

  • 寛政暦

宝暦暦があまりにも酷すぎたので改定された暦。

わりと現役。今でもカレンダーに書いてある「旧暦」が天保暦のことなのです。

最後の暦ということでかなりの高精度だったながら、2033年に突入するとバグります。

現在使われている暦法。日本で制定されたのは明治6年からなので、明治5年以前を正しく算出するには旧暦への変換が必要で面倒です。

なおグレゴリオ暦が完全な暦法かというとそうではなくて、3000年で1日というレベルながらズレがあり、そもそもグレゴリオ暦の根拠となっている太陽年自体が不変ではないという問題がありますが、生きてるうちは関係なさそうです。

その他

週番号

ヨーロッパのカレンダーで使われます。週に連番振ってるだけなんですけど、月曜始まりなので注意です。

iOS5と6とで、NSDateFormatterYYYYyyyyで取得できる値が変わる現象があって、YYYYの方は週番号ベースだから年が違うみたいなstackoverflow見て信じてたんですけど、あれただのバグだったんですね…(iOS7で直った。でもyyyyを使いましょう)。

iOS8で使えるカレンダー

これらは西暦カレンダーに併記されるという、雑な扱いを受けています。

  • 中国暦

民国紀元のことかと思ったら、時憲暦っぽい。

ヒジュラ暦っぽい。

こちらは普通にユダヤ暦

Androidのカレンダー

Javaは1.4で仏暦を、1.6で和暦をサポートしました。

しかし AndroidJavaじゃない ので、実装を見る限り両方とも使えなさそうです。

今でも和暦を使うところって結構多い(行政とか銀行とか保険屋とか)と思うんですけど、JapaneseImperialCalendarがないAndroidで和暦が使えなくて困った、みたいな話はあんまり聞かないですね。

マテリアルなナビゲーションドロワー。

今のGoogle製のアプリはおおよそこういう見た目になっています。

記事の端末は若干古く、記事のコメントにあるSSのようにステータスバー領域までナビゲーションドロワーが被ってくるようなのがマテリアルなやつらしいです。

で、これどう作るんでしょうね。

NavigationDrawerテンプレート

NavigationDrawerテンプレートでプロジェクトを作ってもこういう動きにはなりません。それどころか、ハンバーガーボタンが動かなくて違和感が酷いです。(自分の環境が最新になってないだけだったらすみません)

これはandroid.support.v4.app.ActionBarDrawerToggleが廃止され、android.support.v7.app.ActionBarDrawerToggleへ移行したのが原因です。

ドキュメントがまだ整備されていなくて、Creating a Navigation Drawer | Android Developersの記述も古いままなんですよね…。

ic_drawer.pngをゴミ箱に投げ捨てて、インポートするActionBarDrawerTogglev4からv7に変えましょう。

//android.support.v7.ActionBarDrawerToggle
mDrawerToggle = new ActionBarDrawerToggle(getActivity(), 
    mDrawerLayout, 
    R.string.navigation_drawer_open, 
    R.string.navigation_drawer_close)

ただこれも過渡期の、ハンバーガーボタンが矢印にアニメーションするやつで、今風ではないのです。

ActionBarの上にドロワーを載せるには?

どう頑張っても無理だと思うんです。ビュー階層がそもそも違うので。

アプリケーションの最上層には、Window#getDecorView()で取得できる、android:id/dector_content_parentがあって、android:id/action_bar_containerandroid:id/contentxmlで指定したレイアウト)はその子要素となっているはずなのです。

というわけで、適当にGmailアプリのビュー階層をダンプしてみました。

f:id:quesera2:20150110133817p:plain

com.google.android.gm:id/action_bar_rootの下にandroid:id/contentがあります。ドロワーはその子要素となっています。

ActionBarっぽく見えてるのはさらにその下にあるcom.google.android.gm:id/mail_toolbarというただのViewです。

これはサポートパッケージのActionBarActivityを使った上で、

<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>

ActionBarを消し去った上で、さらに代替としてToolbarを使っているっ挙動っぽい。

ioschedの実装を読もう

マテリアルデザインのお手本とされているioschedを読んでみます。

google/iosched · GitHub

前読んだときは、UI部分はスルーしてました。

src/main/res/values/styles.xmlより、

<style name="Theme.IOSched.Base" parent="Theme">
  ...(省略)
  <item name="windowActionBar">false</item>
  <item name="android:windowNoTitle">true</item>
  ...(省略)
</style>

<style name="Theme.IOSched.WithNavDrawer" parent="Theme.IOSched" />

src/main/res/values-v21/styles.xmlより、

<style name="Theme.IOSched.WithNavDrawer" parent="Theme.IOSched">
  <item name="android:statusBarColor">@android:color/transparent</item>
</style>

アクションバー消してるのは予想通り。stylesで空のテーマを作っておいて、v21だけステータスバーを透明にしてます。v21なんですね。4.4のimmersive mode(あまり知らない)だと思ってたので意外。

src/main/com/google.samples/apps/iosched/ui/BaseActivity.javaより、

// Primary toolbar and drawer toggle
private Toolbar mActionBarToolbar;

protected Toolbar getActionBarToolbar() {
  if (mActionBarToolbar == null) {
    mActionBarToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar);
      if (mActionBarToolbar != null) {
        setSupportActionBar(mActionBarToolbar);
      }
    }
  return mActionBarToolbar;
}

ですよねー。

setSupportActionBar()ってもうこのために存在するメソッドとしか思えませんでしたし…。

setSupportActionBar()というのは、引数に渡したandroid.support.v7.widget.ToolbarをActionBarとして利用するメソッドです。

本物のActionBarをThemeで非表示にした上で、レイアウト上にあるToolbarをActionBarとして利用することによって、画面全体にナビゲーションドロワーが表示されるようにしているのです。

private void goToNavDrawerItem(int item) {
  Intent intent;
  switch (item) {
    case NAVDRAWER_ITEM_MY_SCHEDULE:
      intent = new Intent(this, MyScheduleActivity.class);
      startActivity(intent);
      finish();
      break;
...(以下略)

ドロワーからアイテム選択されたらstartActivity()した後、自身をfinish()してます。

つまり、ドロワーのルートになる全てのレイアウトがToolbarNavigationDrawerのリスト部分をincludeしてるんですね。ドロワー内のリストの選択状態は各Activityで管理なんでしょうか。

ドロワーが閉じてる最中にstartActivity()したら見た目の整合性取れないよね?と思ったら、postDelayed()startActivity()にディレイを掛けて、ドロワーが閉じた後に移動するよう見せかけてます。

これ微妙すぎるのでは…。

ActionBarActivityを使わない場合

Activity | Android Developers

API Lv21以降はsetActionBar()setSupportActionBar()と同じことができます。

ただし、この引数のToolbarandroid.support.v7.widget.Toolbarではなく、 android.widget.Toolbarです。両者は全く別物です…。

Androidのサポートパッケージについて

v17 Leanback Libraryの存在はとりあえず無視しておくとして、Androidのサポートパッケージは何種類かあります。

たぶんv13パッケージはなぜ存在するのか知らない人が多いと思いますので備忘録も兼ねて。

v4 Support Library

まず、サポートパッケージのv4とは「サポートパッケージのversion4」ではなく、「AndroidAPI Lv4以上で利用できるサポートパッケージ」を意味しています。

v4はAndroid 1.6 Donut以上で使えるライブラリで以下の機能を持ちます。

互換性のための機能

  • 3.0で追加されたFragmentのサポート
  • 3.0で追加されたLoaderのサポート
  • Notification系のメソッド@Deprecatedが多く、通知できるスタイルも4.1で大幅に増えたのですが、NotificationCompatを使うことでその辺をそれほど意識しなくて済む…かも?

新機能

  • LocalBroadcastManager。ブロードキャストレシーバーは端末全体に通知するけど、「ローカル」な「ブロードキャストレシーバー」はアプリ内部にのみ通知するためセキュア。実装がすごい短くて面白い。アプリ内部で完結する通知を実装したいときにどうぞ。
  • ViewPager。スワイプでページを切り替えるインターフェース。
  • SlidingPaneLayout。マルチペインレイアウトを実現するレイアウト。
  • DrawerLayout。ナビゲーションドロワーの、ドロワーの部分を実現しているレイアウト。「ナビゲーションドロワーにはActionBarが必要」みたいな固定概念がありますが、実はこれ単品でも使えます。

興味がないのであまり知らない

  • アクセシビリティ系の互換ヘルパ
  • FileProvider複数アプリ間でファイルを共有する機能を提供する。標準の連絡帳やギャラリーでは、パーミッションがなくてもIntentで呼び出されたときのみ、ユーザーの選択したアイテムだけ、呼び出し元アプリに一時的にアクセス権限を与える機能があるけれど、それのファイルバージョンみたいなものとふわふわ理解してる。

v7 Support Library

v7はAndroid 2.1 Eclair 以上で使えるライブラリで以下の機能を持ちます。

互換性のための機能

  • 3.0で追加されたActionBarのサポート
  • ActionBarActivity
  • ShareActionProviderSNS連携を実装するときに便利っぽいのだけれど…。

現在は生のActivityを使わずに、ActionBarActivityを継承するスタイルを推奨しています。

この場合アプリテーマはAppCompatを継承する必要があって、これのせいでリソースファイルの見通しが悪くなるのが辛いところ(リソースに名前空間がないから、重複回避にabc__を使ってるせいで候補として真っ先に上がってくるのが辛い)。

AppCompatでいくつかのコンポーネントを作ると、マテリアルな感じにtintカラーを付与してくれます。

  • 3.0で追加されたGridLayoutのサポート
  • ついでにSpaceもバックポート。(GridLayoutのために存在するのだけど、それ以外で使っても割と便利)
  • Switchがようやくバックポート…。

新機能

  • Cardview。カードっぽく見せることができるリストビューのアッパーバージョン。これ必要あります?
  • RecyclerView。削除・追加にアニメーションを付けられるリストビューのアッパーバージョン。5.0以降で使う場合、LayoutManagerを設定しないとクラッシュすることに注意。ただ、LayoutManagerを設定することでグリッド、横方向のリストも作れます。

ListViewに変わるコンポーネントとしていくつか提案されてますが、歴史あるListViewと比べるとノウハウが蓄積されていないので、変なところでハマって無駄な時間を食ったりします。

興味がないので知らない

  • v7 mediarouter library。Choromecastとかその辺?

v8 Support Library

API Lv13から提供された、RenderScriptをv8以降で使えるようにするというものです。

RenderScriptについては…迷走してる感があってもにょい。

この辺、自分の理解度が高いと言えない領域なのでなんとも言えないのですが、当初はOpenGLを置き換えるようなアピールがされてたんですけど、JellyBeanからは描画関連の処理がバッサリカットされて、GPGPUによる高速計算を行うクラスになったっぽい??

あとGPCPUを使えない端末にまでバックポートできてしまうのだけど、そのときは普通にCPU使うらしいですね。

C99頑張って書く労力に見合ってないと思うんですよ…。

v13 Support Library

ついに本題。

v13 Support Libraryって何のためにあるのか知らない人多いと思うんですよ。

これは「v4でバックポートされたFragmentを使わない」ためにあります。

例えば、ViewPagerFragmentPagerAdapterで使う場合を考えると、こいつのコンストラクタandroid.support.v4.app.FragmentManagerを要求するんです。

// OK
mAdapter = new FragmentPageAdapter(getSupportFragmentManager()){
    ...(省略)
});

API Lv13以降の普通のActivitygetFragmentManager()で取得されるFragmentManagerandroid.app.FragmentManagerなので、型の不一致でビルドが通らないわけです。

// NG
mAdapter = new FragmentPageAdapter(getFragmentManager()){
    ...(省略)
});

ではViewPagerを使うときには、必ずFragmentActivityActionbarActivityを使わなければならないのでしょうか?

いいえ、そこでv13サポートライブラリが使えるのです。

//OK
mAdapter = new android.support.v13.app.FragmentPagerAdapter(getFragmentManager()){
    ...(省略)
});

SyncResultメモ。

どうせ自分しか使わないアプリなら、Lollipop限定のJobSchedulerでいいんじゃないかなという気がしてきました。

SyncResultの概要

SyncResult | Android Developersより、必要そうな部分を抜粋。完全な情報はリンク先参照

このクラスはSyncManagerへ同期操作の結果を伝達するために利用される。この値に基づいて、SyncManagerは同期の配置(disposition)と、将来的に新しい同期操作をスケジューリングする必要があるかを決定する。

フィールド

type name description
public boolean databaseError ストレージ層の操作に伴うハードエラーがSyncAdapterに発生したことを示すのに利用します。
public long delayUntil リクエストのAccountAuthorityに一致する将来の同期要求が、少なくともこの秒数だけ遅延させる必要があることを示します。
public boolean fullSyncRequested 設定した場合、SyncManagerはリクエストに利用したのと同じAccountAuthorityと同じ(ただし空のextrasのBundle)で即座に同期を要求します。
public final SyncStats stats 同期操作について、追加の統計情報を保持するために利用します。
public final boolean syncAlreadyInProgress SyncAdapterが既に同期操作を実行していることを示すために利用します。必ずしもAccountAuthorityの要求が処理できたわけではありません(?)。(原文:Used to indicate that the SyncAdapter is already performing a sync operation, though not necessarily for the requested account and authority and that it wasn't able to process this request.)
  • syncAlreadyInProgressの翻訳に全然自信がないです。
  • ソフトエラー(通信失敗)とハードエラー(データベースへの書き込み失敗)の2段階でエラーを管理していることが分かります。
  • 上では端折ってますが、"(as defined by the SyncAdapter)"の箇所はSyncAdapterが定義するので、プログラマが制御する必要はなさそうです。逆に言えば上記のフィールドはユーザー側である程度制御する必要があるのだと思います。

SyncStatsの概要

SyncStats | Android Developersより、必要そうな箇所を抜粋。この辺日本語ソースの情報は、50 Android Hacksという翻訳本にちょっとだけ載っているのを確認したくらいで、ドキュメントもtypoが多い…。

雑訳なので、ちゃんとした情報はリンク先参照。

同期操作の結果について、さまざまな統計を記録するのに利用します。SyncManagerSyncResultを経由してこれらへアクセスし、同期の配置(disposition)を決定するため、いくつかの情報を使用しています。

SyncManagerがこれらの値をどう扱うのかについて、更なる議論はSyncResultを参照してください。

フィールド

type name description
public long numAuthExceptions ハードエラー。リクエストで指定されたAccountでの認証に失敗した。問題を解決するために、ユーザーの何らかの操作が必要であることを意味する。
public long numConflictDetectedExceptions ハードエラー。サーバー上のリソースを更新または削除しようとしたとき、回復不能なバージョン競合が発生した。ローカルストレージをクリアして最初からやり直すようユーザーの介入が必要かもしれませんが、サーバーから新しい状態を取得することで解決することが期待される。
public long numIoExceptions ソフトエラー。ネットワークの接続、またはタイムアウトなどの問題が発生した場合など、再試行すれば成功する可能性があるリクエストである。
public long numParseExceptions ハードエラー。サーバーまたはそれ以降のストレージから受信したデータで問題が発生した。
public long numSkippedEntries 何件入力されたか追跡するためのカウンターで、同期操作中にサーバーまたはローカルストアに無視された件数を意味する。解析不能なデータを検出しても、即座に失敗とせず、スキップして次へ進めることを決めた場合。
  • エントリ(numEntries)=挿入件数(numInserts)+更新件数(numUpdates)+削除件数(numDeletes)という解釈でいいんだろうか。これらは"as defined by the SyncAdapter"とあるので、設定してはいけない。
  • numSkippedEntriesはサーバー側起因で解析不能なデータを処理した場合のみ設定するのか、CONFLICT_IGNOREで重複データを無視した場合も含めていいのか説明が曖昧。numSkippedEntriesを加算してるコードを検索しても3件しか出てこない…。
  • numParseExceptionsは詳細な説明と例示が書かれているのですけど、「サーバーから不正なエントリを受信した場合、そのエントリを削除して進捗を進めた上で、エラーが発生したことを記録するためにnumParseExceptionsを加算することができる」という感じで、実装者の判断に委ねる感じ。

同期結果

クリーン、ソフトエラー、ハードエラーの3種の結果になります。

クリーン

何の問題もなく同期に成功したことを意味します。

生成された時点のSyncResult()の結果はクリーンです。

またclear()メソッドを呼び出すことでクリーンにすることができますが、syncAlreadyInProgressが設定されている状態でclear()を呼び出すと、UnsupportedOperationExceptionがスローされます。

ソフトエラー

軽微なエラーが生じたことを意味し、指数バックオフで再試行します。

いずれかがtrueのとき、ソフトエラーと判定されます。

  • numIoExceptions > 0
  • syncAlreadyInProgress

なお、ソフトエラーとハードエラーの条件を同時に満たした場合、ハードエラー扱いになります。

ハードエラー

重篤なエラーが生じていることを意味し、SYNC_EXTRAS_UPLOADフラグが設定されている場合を除き、再試行しません。

いずれかがtrueのとき、ハードエラーと判定されます。

  • numParseExceptions > 0
  • numConflictDetectedExceptions > 0
  • numAuthExceptions > 0
  • tooManyDeletions
  • tooManyRetries
  • databaseError

SyncAdapterのメモ。

年末暇なAndroid開発者は是非SyncAdapterを実装しましょう。

バックグラウンドフェッチはこれでやるべきです。

startForeground()とかStickyBroadcastを悪用した死ににくいサービスとかで延々通信させる闇アプリは滅びてください。ていうか他人が実装した通信処理を信用できないので、バックグラウンドで何か通信するアプリは極力入れない主義です。

しかしSyncAdapterは本当に資料がないです。以下、実装するときにいくつか疑問だったことのメモです。

android:process=":sync"

SyncAdapterのトリガとなるサービスには:syncというプロセス名を付けなければいけません。

<service> | Android Developers

If the name assigned to this attribute begins with a colon (':'), a new process, private to the application, is created when it's needed and the service runs in that process.

:から始まるプロセス名はそのアプリのプライベートプロセスとして起動されることを意味します。そうでない場合、グローバルプロセスになります。

実動作を見てみると、hogehoge.yourpackage:syncというプロセス名で同期処理が行われていることを確認できると思います。ここまでは知ってる人も多いかと思います。

Androidが闇なのは、ここで「グローバルプロセスとプライベートプロセスって何が違うの?」という当然の疑問を抱いても、それを知ってる人がまずいないことです…。

ContentProviderClient

これがずっと疑問で、Stackoverflow日本語版のおかげで解決したんですけど、

SyncAdapterのサンプルだとContentResolverをプライベート変数にしたり、onPerformSync()のタイミングで取得したりしてますけど、単一のContentProviderを使う限りにおいては、引数で取得できるContentProviderClientを利用した方が高速です。

@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
  // フェッチを実行する、結果が取得できなければ更新しない
  List<FeedItem> result = FeedConnection.execute();
  if (result == null) {
     syncResult.stats.numIoExceptions++;
     return;
  }

  try {
    ContentValues[] values = FeedDAO.getInstance().convertFromFeedList(result);
    provider.bulkInsert(Constants.CONTENT_URI, values);
  } catch (RemoteException e) {
    syncResult.databaseError = true;
  }
}

また、バルクインサートを使うとより効率的です。

テーブルを正規化してたりすると、複数ContentProviderを扱うことになると思いますので、この手は使えないのですけど。

DAO は自作しない方がいいです

Data Access Objectとかgetter/setterとか人間が書くものじゃない…。

素直にLombokとORMで楽しましょう。

…と考えてるのですけど、どのORMを使うかが難しい。

ActiveAndroidは見えてる地雷っぽいので回避したいです。greenDaoがよさげなのですけど、資料の漁りやすさに難がありそう。

AuthenticatorもContentProviderもスタブ実装でいい

SyncAdapterが面倒なのは、AuthenticatorContentProviderを実装しなければいけないという点で、AccountManagerなど前提知識が多くこの辺で詰まる人が多いと思いますけど、これらを実装する必要はありません

しかしContentProviderは実装しておくといいかもしれないです。DataBaseHelperを直接利用したほうが利便性は高いですし、開発者全体のレベル感に合わせて敢えて採用しないみたいなケースがありがちで、ちゃんとContentProviderを実装する経験というのは意外に得がたいです。

CursorAdapterCursorLoaderと連携させると非同期でデータを更新し、アップデートの通知を受け取って自動的に更新されるタイムラインが感動的なくらいに楽に作れます!(CursorAdapterRecyclerViewの相性が悪いことには目を背けつつ…)

スタブ実装で構わないということは、これらは不要なのでしょうか?

そうではなく、両者を実装することによってセキュアな同期処理を行えるのです。

SyncAdapterによって、システムがContentProviderを更新する」という説明からセキュリティ的にどうなの?と思うかもしれませんが、そのためのAuthenticatorです。AccountManagerによって認可された権限でContentProviderを操作することこそが、SyncAdapterの肝なのです。

たぶん。

SyncResultって何書けばいいの?

SyncAdapterを使えばいつ同期を行えばいいのか、システムが面倒を見てくれる」という触れ込みですが、これはSyncResultに結果を返したケースだと考えています。

SyncResultをちゃんと書くことで、システムに「いつ次の同期を行えばいいのか」の判断を与えることができます。

でも公式ドキュメントではこのことが完全にスルーされており、どう書けばいいのやら、他の実装を参考にしたり、実際の同期処理がいつ走っているのか確かめて手探りでやる必要があります…。

どうデバッグすればいいの?

SyncAdapterによる同期処理は別プロセスで起動するため、単にブレークポイントを貼ってもスルーされます。:syncのプロセスが起動したタイミングでDDMSからデバッガをアタッチする必要があります。

が、そんなことやってられません。

いつ同期されたのか、同期したときにエラーがなかったかなど、診断ログを出力する機能を作っておくのが一番いいのかなーと思ってます。