結局AndroidX対応は簡単なので身構える必要はないが、外的要因で対応が前倒しになりうる

仕事でちょっと困ったので、調査ログを残しておく。

長い長い前提知識

そもそもAndroidXとはなんなのか、Jetifierとはなんなのかを記しておく。

AndroidXの成り立ち

前提知識として、Android開発における最も一般的な外部ライブラリとして Android Support Library / AndroidX というものが存在する。
もともとAndroidには Android Support Library というものがあって、これの後述する問題を解消したものが AndroidX である。

どの程度一般的かというと、Android Studioで新規にプロジェクトを作成するとそもそも標準で入ってくるし、Googleの公開している (Googleのビジネスに依存した) Android向けライブラリでは当然のごとく依存関係に含まれている。
というのも、Support LibraryしかりAndroidXが目指しているのは「Android OSから独立した、Androidのリリース全体に渡る下位互換性の提供」であり、アプリが実行されるOSバージョン自体がどんなバージョンであってもつつがなく動作することを保証するためのライブラリであるからだ。

すなわち特定バージョンのAndroidリリースと同期的に変更されるアプリ、具体的にはOS標準のシステムアプリなどでない限り、アプリはネイティブのAPIを直接叩くのではなく、Support LibraryしかりAndroidXのAPIを利用すべきである。

冒頭の「なぜAndroidXが登場したのか」という部分だが、これは

  • 最低サポートバージョンごとにモジュールが分離されており、ファットになっていた
  • モジュール間が密に結合していることもあり、想定バージョンごとにmajorバージョンをまとめて全部上げる方針になっていた

などのSupport Libraryの問題を解消すべく、

  • 機能ごとにモジュールを分離した
  • モジュールごとに独立したsemverを採用した

をはじめとする複数の修正を行いイチから再スタートしたのがAndroidX、ということになる。

Jetifierとはなにか

当然ながら、AndroidXはこれから更新が重ねられていく。すなわち新たな機能を利用したい場合、AndroidXへの移行を余儀なくされる。
しかしながらメンテナンスの停止したライブラリを利用しているだとか、アプリ自身が独立してAndroidXの機能をいち早く利用したいだとかの場合、

  • Support Libraryへ依存しているライブラリ実装
  • AndroidXへ依存しているライブラリ実装
  • AndroidXへ依存しているアプリ実装

などが混在してしまう可能性がある。

これを解決するため、GoogleがJetifierというツールを提供している。これを用いると、Support Library v28を利用していることを前提にそれらのクラス参照たちをAndroidXのものへ繋ぎ変えてくれる。スタンドアロン版を直接利用することも可能だが、ビルドスクリプトでフラグを立てるだけで自動で実行させることができる。

AndroidX対応はいつやるのか

本題。これらの前提のもと、じゃあいつ自分たちの作っているアプリでAndroidXに対応するのか、という話がいつかは出てくる。
実際自分の関わっているいくつかの事業者を見ていると、AndroidXへの本格対応は未だ行っていないケースが散見される。

いくらでも例外はあるものの、筆者としては「早めにやるに越したことはないが、最悪AndroidXの機能が必要になるまではJetifier以上の対応はしなくてよい」という意見を推していきたい。
この「AndroidXの機能が必要になるまで」に外的要因も含まれうるという点で、いくらかの危険性をはらんでいることも無視できないのが難しい部分だ。

Android Pluginの挙動

下記のようなアプリがあるとする。このアプリはSupport Library v28の機能に依存していて、具体的には古いAndroid OSにおけるbackgroundTintの実現にAppCompatを利用している。
この機能の実現にはAndroidXでの更新以降の機能は要求しておらず、Support Libraryとして提供されていた期間までの機能で必要十分である。

github.com

この時点での依存関係ツリー

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 821ms
# 1 actionable task: 1 executed

このアプリでandroid.useAndroidXをenableにしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
 android.enableJetifier=false

この時点ではアプリ側のコードを書き換えずビルドが可能である。それもそのはず、依存関係の書き換えは行われないのだ。

このdiffにおける依存関係

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

ではこのフラグはどのような影響を与えるのか?答えはAndroid Developersの記述が全てだ。

android.useAndroidX: true に設定すると、Android プラグインは Support Library ではなく、該当する AndroidX ライブラリを使用します。設定しない場合、このフラグはデフォルトで false です。

Android Pluginが当該ライブラリを利用する箇所としてわかりやすいものには、DataBindingが存在する。たとえば下記のようなDataBindingを用いたアプリの場合。

github.com

この時点でのdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- com.android.databinding:baseLibrary:3.5.0
# +--- com.android.databinding:library:3.5.0
# |    +--- android.arch.lifecycle:runtime:1.0.3 -> 1.1.1
# |    |    +--- android.arch.lifecycle:common:1.1.1
# |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    +--- android.arch.core:common:1.1.1
# |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    +--- com.android.support:support-core-utils:26.1.0 -> 28.0.0
# |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    +--- com.android.support:support-compat:28.0.0
# |    |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- com.android.support:collections:28.0.0
# |    |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- android.arch.lifecycle:runtime:1.1.1 (*)
# |    |    |    \--- com.android.support:versionedparcelable:28.0.0
# |    |    |         +--- com.android.support:support-annotations:28.0.0
# |    |    |         \--- com.android.support:collections:28.0.0 (*)
# |    |    +--- com.android.support:documentfile:28.0.0
# |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    +--- com.android.support:loader:28.0.0
# |    |    |    +--- com.android.support:support-annotations:28.0.0
# |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
# |    |    |    +--- android.arch.lifecycle:livedata:1.1.1
# |    |    |    |    +--- android.arch.core:runtime:1.1.1
# |    |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
# |    |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
# |    |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
# |    |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
# |    |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
# |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
# |    |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
# |    |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
# |    |    +--- com.android.support:localbroadcastmanager:28.0.0
# |    |    |    \--- com.android.support:support-annotations:28.0.0
# |    |    \--- com.android.support:print:28.0.0
# |    |         \--- com.android.support:support-annotations:28.0.0
# |    \--- com.android.databinding:baseLibrary:3.5.0
# +--- com.android.databinding:adapters:3.5.0
# |    +--- com.android.databinding:baseLibrary:3.5.0
# |    \--- com.android.databinding:library:3.5.0 (*)
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0 (*)
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 0s
# 1 actionable task: 1 executed

useAndroidXをenabledにしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
 android.enableJetifier=false

この状態でビルドを試みると失敗することが確認できるだろう。

./gradlew :app:buildDebug --console=plain
# > Task :app:preBuild UP-TO-DATE
# > Task :app:preDebugBuild UP-TO-DATE
# > Task :app:compileDebugRenderscript NO-SOURCE
# > Task :app:generateDebugResValues UP-TO-DATE
# > Task :app:generateDebugResources UP-TO-DATE
# > Task :app:mergeDebugResources UP-TO-DATE
# > Task :app:checkDebugManifest UP-TO-DATE
# > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
# > Task :app:mainApkListPersistenceDebug UP-TO-DATE
# > Task :app:processDebugManifest UP-TO-DATE
# > Task :app:bundleDebugResources UP-TO-DATE
# > Task :app:mergeDebugShaders UP-TO-DATE
# > Task :app:compileDebugShaders UP-TO-DATE
# > Task :app:generateDebugAssets UP-TO-DATE
# > Task :app:mergeDebugAssets UP-TO-DATE
# > Task :app:compileDebugAidl NO-SOURCE
# > Task :app:generateDebugBuildConfig UP-TO-DATE
# > Task :app:prepareLintJar UP-TO-DATE
# > Task :app:prepareLintJarForPublish UP-TO-DATE
# > Task :app:generateDebugSources UP-TO-DATE
# > Task :app:dataBindingExportBuildInfoDebug UP-TO-DATE
# > Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
# > Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE
# > Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE
# > Task :app:processDebugResources UP-TO-DATE
#
# > Task :app:compileDebugKotlin FAILED
# e: /Users/s-yoshioka/Documents/android-databinding-support-library-app-example/app/src/main/java/jp/s64/example/android/databindingsupportlibraryapp/MyActivity.kt: (3, 16): Unresolved reference: databinding
# e: /Users/s-yoshioka/Documents/android-databinding-support-library-app-example/app/src/main/java/jp/s64/example/android/databindingsupportlibraryapp/MyActivity.kt: (14, 19): Unresolved reference: DataBindingUtil
#
# FAILURE: Build failed with an exception.
#
# * What went wrong:
# Execution failed for task ':app:compileDebugKotlin'.
# > Compilation error. See log for more details
#
# * Try:
# Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
#
# * Get more help at https://help.gradle.org
#
# BUILD FAILED in 2s
# 19 actionable tasks: 1 executed, 18 up-to-date

この時点のdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- androidx.databinding:databinding-common:3.5.0
# +--- androidx.databinding:databinding-runtime:3.5.0
# |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
# |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
# |    |    |    \--- androidx.annotation:annotation:1.0.0
# |    |    +--- androidx.arch.core:core-common:2.0.0
# |    |    |    \--- androidx.annotation:annotation:1.0.0
# |    |    \--- androidx.annotation:annotation:1.0.0
# |    +--- androidx.collection:collection:1.0.0
# |    |    \--- androidx.annotation:annotation:1.0.0
# |    \--- androidx.databinding:databinding-common:3.5.0
# +--- androidx.databinding:databinding-adapters:3.5.0
# |    +--- androidx.databinding:databinding-common:3.5.0
# |    \--- androidx.databinding:databinding-runtime:3.5.0 (*)
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0
#      +--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-compat:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:collections:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- android.arch.lifecycle:runtime:1.1.1
#      |    |    +--- android.arch.lifecycle:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    +--- android.arch.core:common:1.1.1
#      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    \--- com.android.support:versionedparcelable:28.0.0
#      |         +--- com.android.support:support-annotations:28.0.0
#      |         \--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:collections:28.0.0 (*)
#      +--- com.android.support:cursoradapter:28.0.0
#      |    \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-core-utils:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:documentfile:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- android.arch.lifecycle:livedata:1.1.1
#      |    |    |    +--- android.arch.core:runtime:1.1.1
#      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
#      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
#      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
#      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
#      |    |    |    \--- android.arch.core:common:1.1.1 (*)
#      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
#      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
#      |    +--- com.android.support:localbroadcastmanager:28.0.0
#      |    |    \--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:print:28.0.0
#      |         \--- com.android.support:support-annotations:28.0.0
#      +--- com.android.support:support-fragment:28.0.0
#      |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    +--- com.android.support:support-core-ui:28.0.0
#      |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    |    +--- com.android.support:customview:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    +--- com.android.support:viewpager:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:coordinatorlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:drawerlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:slidingpanelayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:customview:28.0.0 (*)
#      |    |    +--- com.android.support:interpolator:28.0.0
#      |    |    |    \--- com.android.support:support-annotations:28.0.0
#      |    |    +--- com.android.support:swiperefreshlayout:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
#      |    |    |    \--- com.android.support:interpolator:28.0.0 (*)
#      |    |    +--- com.android.support:asynclayoutinflater:28.0.0
#      |    |    |    +--- com.android.support:support-annotations:28.0.0
#      |    |    |    \--- com.android.support:support-compat:28.0.0 (*)
#      |    |    \--- com.android.support:cursoradapter:28.0.0 (*)
#      |    +--- com.android.support:support-core-utils:28.0.0 (*)
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    +--- com.android.support:loader:28.0.0 (*)
#      |    \--- android.arch.lifecycle:viewmodel:1.1.1 (*)
#      +--- com.android.support:support-vector-drawable:28.0.0
#      |    +--- com.android.support:support-annotations:28.0.0
#      |    \--- com.android.support:support-compat:28.0.0 (*)
#      \--- com.android.support:animated-vector-drawable:28.0.0
#           +--- com.android.support:support-vector-drawable:28.0.0 (*)
#           \--- com.android.support:support-core-ui:28.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

内部で利用されるライブラリがAndroidXの置き換わるとは、依存関係に下記のような差分が発生することを指しているのである。

10,51c10,23
< +--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:library:3.5.0
< |    +--- android.arch.lifecycle:runtime:1.0.3 -> 1.1.1
< |    |    +--- android.arch.lifecycle:common:1.1.1
< |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    +--- android.arch.core:common:1.1.1
< |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    +--- com.android.support:support-core-utils:26.1.0 -> 28.0.0
< |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    +--- com.android.support:support-compat:28.0.0
< |    |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- com.android.support:collections:28.0.0
< |    |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- android.arch.lifecycle:runtime:1.1.1 (*)
< |    |    |    \--- com.android.support:versionedparcelable:28.0.0
< |    |    |         +--- com.android.support:support-annotations:28.0.0
< |    |    |         \--- com.android.support:collections:28.0.0 (*)
< |    |    +--- com.android.support:documentfile:28.0.0
< |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    +--- com.android.support:loader:28.0.0
< |    |    |    +--- com.android.support:support-annotations:28.0.0
< |    |    |    +--- com.android.support:support-compat:28.0.0 (*)
< |    |    |    +--- android.arch.lifecycle:livedata:1.1.1
< |    |    |    |    +--- android.arch.core:runtime:1.1.1
< |    |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
< |    |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
< |    |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
< |    |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
< |    |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
< |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
< |    |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
< |    |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< |    |    +--- com.android.support:localbroadcastmanager:28.0.0
< |    |    |    \--- com.android.support:support-annotations:28.0.0
< |    |    \--- com.android.support:print:28.0.0
< |    |         \--- com.android.support:support-annotations:28.0.0
< |    \--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:adapters:3.5.0
< |    +--- com.android.databinding:baseLibrary:3.5.0
< |    \--- com.android.databinding:library:3.5.0 (*)
---
> +--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-runtime:3.5.0
> |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
> |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
> |    |    |    \--- androidx.annotation:annotation:1.0.0
> |    |    +--- androidx.arch.core:core-common:2.0.0
> |    |    |    \--- androidx.annotation:annotation:1.0.0
> |    |    \--- androidx.annotation:annotation:1.0.0
> |    +--- androidx.collection:collection:1.0.0
> |    |    \--- androidx.annotation:annotation:1.0.0
> |    \--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-adapters:3.5.0
> |    +--- androidx.databinding:databinding-common:3.5.0
> |    \--- androidx.databinding:databinding-runtime:3.5.0 (*)
58c30,42
<      +--- com.android.support:support-compat:28.0.0 (*)
---
>      +--- com.android.support:support-compat:28.0.0
>      |    +--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:collections:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    +--- android.arch.lifecycle:runtime:1.1.1
>      |    |    +--- android.arch.lifecycle:common:1.1.1
>      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    +--- android.arch.core:common:1.1.1
>      |    |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    \--- com.android.support:versionedparcelable:28.0.0
>      |         +--- com.android.support:support-annotations:28.0.0
>      |         \--- com.android.support:collections:28.0.0 (*)
62c46,68
<      +--- com.android.support:support-core-utils:28.0.0 (*)
---
>      +--- com.android.support:support-core-utils:28.0.0
>      |    +--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:support-compat:28.0.0 (*)
>      |    +--- com.android.support:documentfile:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    +--- com.android.support:loader:28.0.0
>      |    |    +--- com.android.support:support-annotations:28.0.0
>      |    |    +--- com.android.support:support-compat:28.0.0 (*)
>      |    |    +--- android.arch.lifecycle:livedata:1.1.1
>      |    |    |    +--- android.arch.core:runtime:1.1.1
>      |    |    |    |    +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    |    |    |    \--- android.arch.core:common:1.1.1 (*)
>      |    |    |    +--- android.arch.lifecycle:livedata-core:1.1.1
>      |    |    |    |    +--- android.arch.lifecycle:common:1.1.1 (*)
>      |    |    |    |    +--- android.arch.core:common:1.1.1 (*)
>      |    |    |    |    \--- android.arch.core:runtime:1.1.1 (*)
>      |    |    |    \--- android.arch.core:common:1.1.1 (*)
>      |    |    \--- android.arch.lifecycle:viewmodel:1.1.1
>      |    |         \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
>      |    +--- com.android.support:localbroadcastmanager:28.0.0
>      |    |    \--- com.android.support:support-annotations:28.0.0
>      |    \--- com.android.support:print:28.0.0
>      |         \--- com.android.support:support-annotations:28.0.0
113c119
< BUILD SUCCESSFUL in 0s
---
> BUILD SUCCESSFUL in 1s

Jetifierの挙動

android.enableJetifierによる影響はもう少しわかりやすい。たとえば最初に挙げたSupport Libraryによるアプリで有効にしてみる。

diff --git a/gradle.properties b/gradle.properties
index 8964a61..b5dd627 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
 org.gradle.jvmargs=-Xmx1536m
 kotlin.code.style=official
-android.useAndroidX=false
-android.enableJetifier=false
+android.useAndroidX=true
+android.enableJetifier=true

これにより、依存関係ツリーが書き換わる。

この時点でのdependencies

./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
#
# > Task :app:dependencies
#
# ------------------------------------------------------------
# Project :app
# ------------------------------------------------------------
#
# debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target  (androidJvm)).
# +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50
# |    \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.50
# |         +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.3.50
# |         \--- org.jetbrains:annotations:13.0
# \--- com.android.support:appcompat-v7:28.0.0 -> androidx.appcompat:appcompat:1.0.0
#      +--- androidx.annotation:annotation:1.0.0
#      +--- androidx.core:core:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.collection:collection:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.lifecycle:lifecycle-runtime:2.0.0
#      |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.arch.core:core-common:2.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.versionedparcelable:versionedparcelable:1.0.0
#      |         +--- androidx.annotation:annotation:1.0.0
#      |         \--- androidx.collection:collection:1.0.0 (*)
#      +--- androidx.collection:collection:1.0.0 (*)
#      +--- androidx.cursoradapter:cursoradapter:1.0.0
#      |    \--- androidx.annotation:annotation:1.0.0
#      +--- androidx.legacy:legacy-support-core-utils:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.core:core:1.0.0 (*)
#      |    +--- androidx.documentfile:documentfile:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.loader:loader:1.0.0
#      |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.lifecycle:lifecycle-livedata:2.0.0
#      |    |    |    +--- androidx.arch.core:core-runtime:2.0.0
#      |    |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    |    \--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    |    +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
#      |    |    |    |    +--- androidx.lifecycle:lifecycle-common:2.0.0 (*)
#      |    |    |    |    +--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    |    |    \--- androidx.arch.core:core-runtime:2.0.0 (*)
#      |    |    |    \--- androidx.arch.core:core-common:2.0.0 (*)
#      |    |    \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0
#      |    |         \--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
#      |    |    \--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.print:print:1.0.0
#      |         \--- androidx.annotation:annotation:1.0.0
#      +--- androidx.fragment:fragment:1.0.0
#      |    +--- androidx.core:core:1.0.0 (*)
#      |    +--- androidx.legacy:legacy-support-core-ui:1.0.0
#      |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
#      |    |    +--- androidx.customview:customview:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    \--- androidx.core:core:1.0.0 (*)
#      |    |    +--- androidx.viewpager:viewpager:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.drawerlayout:drawerlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.customview:customview:1.0.0 (*)
#      |    |    +--- androidx.interpolator:interpolator:1.0.0
#      |    |    |    \--- androidx.annotation:annotation:1.0.0
#      |    |    +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    +--- androidx.core:core:1.0.0 (*)
#      |    |    |    \--- androidx.interpolator:interpolator:1.0.0 (*)
#      |    |    +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
#      |    |    |    +--- androidx.annotation:annotation:1.0.0
#      |    |    |    \--- androidx.core:core:1.0.0 (*)
#      |    |    \--- androidx.cursoradapter:cursoradapter:1.0.0 (*)
#      |    +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    +--- androidx.loader:loader:1.0.0 (*)
#      |    \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 (*)
#      +--- androidx.vectordrawable:vectordrawable:1.0.0
#      |    +--- androidx.annotation:annotation:1.0.0
#      |    \--- androidx.core:core:1.0.0 (*)
#      \--- androidx.vectordrawable:vectordrawable-animated:1.0.0
#           +--- androidx.vectordrawable:vectordrawable:1.0.0 (*)
#           \--- androidx.legacy:legacy-support-core-ui:1.0.0 (*)
#
# (*) - dependencies omitted (listed previously)
#
# A web-based, searchable dependency report is available by adding the --scan option.
#
# BUILD SUCCESSFUL in 1s
# 1 actionable task: 1 executed

\--- com.android.support:appcompat-v7:28.0.0 -> androidx.appcompat:appcompat:1.0.0

ツリー内のこの箇所にあるとおり、アプリ自身で参照していたSupport LibraryがAndroidXのものに置換されるのである。
しかしプロジェクト内のソースコードや出力されるバイトコードそのものには影響が出ないことには注意が必要だ。すなわち、コードの書換えは必要である。

./gradlew :app:buildDebug --console=plain
# > Task :app:preBuild UP-TO-DATE
# > Task :app:preDebugBuild UP-TO-DATE
# > Task :app:generateDebugResValues UP-TO-DATE
# > Task :app:checkDebugManifest UP-TO-DATE
# > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
# > Task :app:mainApkListPersistenceDebug UP-TO-DATE
# > Task :app:compileDebugRenderscript NO-SOURCE
# > Task :app:generateDebugResources UP-TO-DATE
# > Task :app:mergeDebugResources UP-TO-DATE
# > Task :app:processDebugManifest UP-TO-DATE
# > Task :app:bundleDebugResources UP-TO-DATE
# > Task :app:mergeDebugShaders UP-TO-DATE
# > Task :app:compileDebugShaders UP-TO-DATE
# > Task :app:generateDebugAssets UP-TO-DATE
# > Task :app:mergeDebugAssets UP-TO-DATE
# > Task :app:generateDebugBuildConfig UP-TO-DATE
# > Task :app:compileDebugAidl NO-SOURCE
# > Task :app:processDebugJavaRes NO-SOURCE
# > Task :app:processDebugResources UP-TO-DATE

# > Task :app:compileDebugKotlin FAILED
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (4, 24): Unresolved reference: v7
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (6, 20): Unresolved reference: AppCompatActivity
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (8, 5): 'onCreate' overrides nothing
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (9, 15): Unresolved reference: onCreate
# e: /Users/s-yoshioka/Documents/android-minimum-support-library-app-example/app/src/main/java/jp/s64/example/android/minimumsupportlibraryapp/MyActivity.kt: (10, 9): Unresolved reference: setContentView

# FAILURE: Build failed with an exception.

# * What went wrong:
# Execution failed for task ':app:compileDebugKotlin'.
# > Compilation error. See log for more details

# * Try:
# Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

# * Get more help at https://help.gradle.org

# BUILD FAILED in 1s
# 13 actionable tasks: 1 executed, 12 up-to-date

さて、では依存するライブラリではどうだろうか。たとえばSupport Libraryを依存関係に持つライブラリを追加する。Jetifierを有効にしていない場合は下記のような依存関係が出力される。

\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
     +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
     \--- com.android.support:support-compat:28.0.0 (*)

この時、この依存するライブラリをデコンパイルすると下記のようなものが確認できるだろう。

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package jp.s64.android.toolbox.support.notification

@android.support.annotation.RequiresApi public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { /* compiled code */ }

public fun android.support.v4.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { /* compiled code */ }

ではJetifierを有効にしてみる。当然ネストした依存関係も適切に処理され、下記のようになる。

\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
     +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
     \--- androidx.core:core:1.0.0 (*)

さきほど検証したとおり、自身のプロジェクトにおけるソースコードは書き換わらない。しかしこの依存ライブラリが持つコードをデコンパイルすると、下記のようなものが確認できる。

// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available

package jp.s64.android.toolbox.support.notification

@androidx.annotation.RequiresApi public fun androidx.core.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { /* compiled code */ }

public fun androidx.core.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { /* compiled code */ }

ライブラリ内での参照は自動で書き換わることが確認できた。

アプリにおける基本的な対応

これらを踏まえ対応におけるポイントをまとめると、

  • Support Library 28まで上げておく
  • useAndroidX, enableJetifier を有効にする
  • 壊れたプロジェクト内の参照を修正する
  • 依存するライブラリは自動で書き換わるので気にしなくてよい

の4点を押さえれば十分ということになる。実に簡単だ。

対応の判断

対応は簡単だが、問題はこの判断を行うタイミングや要因である。

  1. AndroidX以降の更新における機能がアプリにおいて必要になった
  2. AndroidX以降の更新におけるBugfix等がアプリに影響があると判明した
  3. 依存するライブラリがSupport LibraryではなくAndroidXを利用する形へ移行した

1は完全にプロジェクトのコントローラブルな範囲であるため割愛する。
2に関しては一般的なライブラリにおける破壊的変更が実施された以降とほぼ同じと捉えてよい。
3はほぼ2と同じ事由になることが想像されるが、少し問題があるため後述する。

これらを総合して言えることは、やはり「やれる内に更新をしていないと工数の確保が難しくなる」という点は他のライブラリと変わらないということだ。
もしアプリのロジックやビジネスが特定ライブラリに密に依存していて、それらのアップデートが必要となった際に雪だるま式にやることが増えてしまうかもしれない。

非アプリにおける対応

では非アプリ、具体的には複数アプリに導入されうるモジュールの場合にはどうだろうか?対応の判断面は変わらない。要は「機能が必要になった」「依存するライブラリが移行した」場合に対応すればよい、ということだ。

しかし「早めに更新したほうがよいか」というと、そんなことはない。むしろ「できる限り対応を遅めたほうがよい」と筆者は考える。

AndroidXとSupport Libraryの間には名前空間やモジュールの分割といった変更があり、大きな変更のように思わせる。
しかし実際には、アプリ側でJetifierを用いることでSupport Libraryからの移行は容易に行える。
では逆にAndroidXに対応したものをSupport Libraryを前提としたアプリで利用するのが容易かというと、これは難しい。JetifierはSupport Libraryを用いた参照をAndroidXのものに変換する機能こそ利用できるが、その逆は困難だからだ。

AndroidXへの対応は簡単だが、それは十分にそのプロジェクトへアサインされたエンジニアのリソースが存在する場合に限られる。現実問題として、必ずしもプロジェクトにフルタイマーや十分な能力を持ったメンバーがアサインされている保証はない。
複数のアプリで利用されるモジュールがAndroidXの利用を要求するようになった場合、必然的にこのモジュールを利用している全てのアプリは移行の作業を行うこととなる。

「ではAndroidX対応しなくてもいいのか?」と聞かれそうになる。この答えはYesだ。なぜならSupport Libraryさえ使っていれば、実際にどちらを利用するかはアプリ側に判断を委ねることができるからだ。Jetifierを有効にするだけでよい。

まとめ

長々と書いてきたが、この記事における主張は下記のとおりだ。

  • アプリにおけるAndroidX対応は、最悪必要になるまで対応しないという選択が可能である。が、外的要因に影響されないよう早めにやればよいし、通常は依存するライブラリに関わらず今からでも対応可能である。
  • 非アプリにおける対応は、可能な限り遅めたほうがよい。Support Libraryを使っている限りは実際のアプリに判断を委ねることができる。

筆者は業務でエンドユーザ向けアプリからSDKまで様々な開発に携わっているが、AndroidXに関する対応は事業者によってまちまちである。
しかしながらエンジニアを対象としたプロダクトを作る事業者においては、ユーザであるエンジニアの利便性を損なわない対応を求めたいものだ。

おまけ: 実はde-jetifierは存在する

AndroidXからSupport Libraryの利用へ戻すのは困難と記述したが、困難ではあっても可能ではある。
JetifierはAndroid Gradle Pluginと統合されているが、CLIからも利用ができる。そしてこのCLI版には Reverse mode が存在している。

developer.android.com

依存先ライブラリがAndroidXへ移行してしまったが、実態としてAndroidXの機能を利用していない (Support Library時代に提供されていた機能の範囲に留まっている) 場合は、これを利用するのもひとつの手段かもしれない。

Mavenリポジトリの指定は記述順序を考えないと解決時のパフォーマンスがじわじわ下がっていく

Androidアプリ開発などをしていれば、大概のプロジェクトはGradleを用いてライブラリの依存関係を解決しようとすることになる。そして開発・運用期間の長いアプリになればなるほど、後追いで導入されたサードパーティ製ライブラリとその取得元のMavenリポジトリ定義が肥大化していくケースが散見される。

普段なんとなく設定しているMavenリポジトリの定義だが、実はビルド時のパフォーマンス低下の原因となるケースがあるため、注意が必要だ。


たとえば、以下のようなスクリプトがあるとする。追加してあるライブラリ郡にはさほど意味がなく、単に大量のライブラリが必要な状態を再現するための記述である:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

android {
    ...
}

repositories {
    google()
    jcenter()
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.arch.core:core-common:2.0.1'
    implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
    implementation 'androidx.browser:browser:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.collection:collection:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.contentpager:contentpager:1.0.0'
    implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
    implementation 'androidx.cursoradapter:cursoradapter:1.0.0'
    implementation 'androidx.customview:customview:1.0.0'
    implementation 'androidx.documentfile:documentfile:1.0.1'
    implementation 'androidx.drawerlayout:drawerlayout:1.0.0'
    implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
    implementation 'androidx.emoji:emoji:1.0.0'
    implementation 'androidx.exifinterface:exifinterface:1.0.0'
}

これをビルドしてみる。

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 13:48:45
# ... 
# 13:50:50

125秒。


ここに、参照するライブラリは変えずに "リポジトリだけ" 追加する。今回は仮に実在しないリポジトリを追加してみた:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    google()
    jcenter()
}
...

この状態で、さきほど同様に実行する:

export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:00:41
# ... 
# 14:03:49

188秒。この程度なら誤差かもしれない。


では、大量に存在するとどうだろう。今回も、仮に実在しないリポジトリを追加する:

...
repositories {
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
    google()
    jcenter()
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:07:55
# ...
# 14:13:33

338秒。元の125秒比較すると実に2.7倍近い時間が掛かるようになってしまった。


試しに順序を変えてみる。実在しないリポジトリたちは後ろへ移動する:

...
repositories {
    google()
    jcenter()
    maven { url 'https://example.com/m2-repo-1' }
    maven { url 'https://example.com/m2-repo-2' }
    maven { url 'https://example.com/m2-repo-3' }
    maven { url 'https://example.com/m2-repo-4' }
    maven { url 'https://example.com/m2-repo-5' }
}
...
export TZ="Asia/Tokyo"
$(rm ./local.properties || true) && ./gradlew clean && date +%T && ./gradlew build --debug && date +%T
# rm: cannot remove './local.properties': No such file or directory
# Downloading https://services.gradle.org/distributions/gradle-5.1.1-all.zip
# ...
# 14:17:31
# ...
# 14:19:34

123秒。リポジトリ自体は定義されているにも関わらず、元の定義に匹敵する速度まで回復した。


なぜこんな現象が起こるのかは、ログを確認すればわかる。 338秒という最も長い時間の必要だったビルドにおけるログを確認してみると、下記のような出力が散見される:

...
05:39:19.831 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:19.955 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-1/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...
05:39:20.465 [DEBUG] [org.apache.http.impl.execchain.MainClientExec] Executing request GET /m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom HTTP/1.1
...
05:39:20.586 [INFO] [org.gradle.internal.resource.transport.http.HttpClientHelper] Resource missing. [HTTP GET: https://example.com/m2-repo-2/androidx/annotation/annotation/1.1.0/annotation-1.1.0.pom]
...

m2-repo-1, m2-repo-2... と順にリポジトリを手繰っている。repositories {}で定義するリポジトリは、純粋に上から下へ優先順位を持っており、それを順々に参照するのである。
これを自身の参照したいライブラリ毎に行うため、当然ながらじわじわと解決の速度を低下させる要因になりうるのである。


広告系のSDKなどの特定の事業に依存したライブラリの多くは、Maven CentralやJCenterのようなパブリックリポジトリではなく 何かしらのプライベートリポジトリを利用している。特に複数のライブラリを併用する場合は、この参照先リポジトリがどんどん肥大化していくことが多い。

依存解決のためのリクエストは 成功するまで順々に投げられていくため、もし上位に(たとえば1つのライブラリしか置かれていないような)ほとんど使われないリポジトリが指定されていれば、すべての依存関係解決のために毎回無駄なリクエストを投げることになってしまう。


よって、このリポジトリ定義の順序は、下記の優先順位で決定するとよい:

  1. 特別な理由で最優先にしなければならないリポジトリ / 信頼性の高いリポジトリ
  2. 自身のアプリが持つ依存関係の中で最も利用数が多いリポジトリ

この記事の趣旨で言うと、本来であれば2だけを気にすればよい。


が、Android開発などの場合には特別な事情があるため併記しておく。

JCenter(JFrog Bintray)ではGoogle Maven Repositoryのミラーリング(Proxyではない)を独自に行っているが、一時Google Maven Repository内に配置してあるChecksumが不正な内容になってしまった時がある。
JCenterは仕組み上Checksumが不正な場合には404ではなく409を返すようになっている。その結果、google()よりもjcenter()の優先順位を高く設定していた場合には依存関係の解決に失敗してしまう、という問題が発生したことが過去にある

ライブラリの参照元は原則として発行元の推奨するものを利用すべきであるため、Android開発においては(たとえほとんどのライブラリが他のリポジトリ由来であっても)Google Maven Repositoryを最優先にすべきである。

FCM用のFirebaseプロジェクトを別プロジェクトへ移行する

AndroidアプリでPush通知を行う場合、Firebase Cloud Messaging (旧Google Cloud Messaging) を経由しGoogle Play Servicesがよしなに処理してくれるよう投げることになる。
FCMを使うということは、すなわちFirebaseのプロジェクトを firebase.google.com 上で作成するということになる。

アプリ開発の現場でよくあるケースに、当初はAndroid用のPush通知目的でのみ作成したFirebaseプロジェクトを、iOS含めAnalyticsやRemote Configを本格的に利用しようとした時にエイヤッと統合されたものへ移行する、というものがある。

この記事は、既にFCMを利用しているアプリのプロジェクトを移行できるのか検証した時のログである。

前提1

アプリ自体のリニューアル(package nameの変更)は行わないという前提のため、APK署名時に使う証明書は同じものとする。

前提2

今回移行するアプリは、以下のように設定して試した:

プロジェクト名 mynotifymigrationapp-orgproj
プロジェクトID mynotifymigrationapp-orgproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp
アプリのニックネーム (省略可) MyNotifyMigrationApp (oldproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
credential登録方法 google-services.jsonをアプリ内に配置し、google-servicesプラグインで認証
SHA証明書フィンガープリント(※リリース用) EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3
通知の利用 数回なげて、受信できることを確認した

1. 旧プロジェクトから証明書のハッシュを削除する

FirebaseがInvites, Dynamic Links, Authenticationなどの機能を利用するアプリが不正なものではないことを確認できるのは、Googleのサーバにpackage nameと証明書フィンガープリントのペアをグローバルに登録し、それと一致することを条件とするため。
よって、FCMではマストではないSHA1フィンガープリントの登録ではあるが、他のプロジェクト(他の人物、というとわかりやすいか?)が同一の組み合わせを登録しようとするとエラーにで弾かれるようになっている。

すなわち一番はじめにすることは、旧プロジェクトに紐付いた証明書に関する設定を削除すること。

旧プロジェクトのProject Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (oldproj)) 内に SHA 証明書フィンガープリント という項目があるため、この内容を削除する。

今回の場合は、

  • 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5
  • EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3

の2件。

2. アプリの認証情報を削除する

この時点で既に旧アプリでの認証情報は不要になるため、削除してしまう。
アプリ内に配置したgoogle-services.jsonを削除して、おしまい。

3. 移行先プロジェクトを作成する

既に移行先プロジェクトが存在する場合はスキップ。
今回は以下の内容にした:

プロジェクト名 mynotifymigrationapp-newproj
プロジェクトID mynotifymigrationapp-newproj-1
Androidパッケージ名 jp.s64.android.example.mynotifymigrationapp ※元と同じ
アプリのニックネーム (省略可) MyNotifyMigrationApp (newproj)
デバッグ用の署名証明書 SHA-1(省略可) 89:E6:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:34:E5 ※元と同じ

4. 新たな認証情報をアプリに入れる

作成の途中でgoogle-services.jsonをダウンロードすることができるので、これをアプリに配置しておく。ステップ2で削除した分。

5. リリース用のハッシュをプロジェクトに追加する

ステップ1で削除した分。移行先プロジェクトの Project Overview -> Settings -> プロジェクトの設定 -> 全般 -> マイアプリ -> Android アプリ -> 当該アプリ(MyNotifyMigrationApp (newproj)) に、本番の証明書から取ったSHA1ハッシュを追加する。
今回の場合は EA:F3:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:**:71:C3 となる。

6. 必要なら、FCMのサーバーキーを変更する

Amazon SNSやReproなどを使っている場合、FCMへAPI経由でアクセスするためのサーバーキーを新しいものへ変更する必要がある。
Project Overview -> Settings -> プロジェクトの設定 -> クラウドメッセージング -> プロジェクト認証情報 から取得できるので、これを用いて変更作業をする。

Proxyで傍受可能なオレオレ証明書を通せるAndroidアプリの作り方

人間誰しも1度はProxyを通して通信内容をデバッグしたい時がある。
要はオレオレ証明書を発行して間に挟めば傍受も改ざんもやりたい放題なのだが、Android 7以降からは一筋縄では実現できない。

In Android Nougat, we’ve changed how Android handles trusted certificate authorities (CAs) to provide safer defaults for secure app traffic. Most apps and users should not be affected by these changes or need to take any action. The changes include:

  • Safe and easy APIs to trust custom CAs.
  • Apps that target API Level 24 and above no longer trust user or admin-added CAs for secure connections, by default.
  • All devices running Android Nougat offer the same standardized set of system CAs—no device-specific customizations.

https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html

上記のとおり、AOSPで設定されたCA以外は信頼されず、ユーザが独自にインストールしたCAすらもデフォルトで無効になる。
しかしこれらをオプトインで利用することも当然可能になっており、またアプリ内にderファイルを同梱させ利用させることも可能だ。

これらは設定ファイルを記述して自分のアプリに組み込めば実現できる。

今回目指すこと

今回は、

  • Proxyサーバが、アプリが要求した https://example.com の内容を傍受できるようにする
  • 上記の内容を改竄し、アプリがへ届けられるようにする

の2つの実現を目指す。
ただし、傍受や改竄自体は便利なアプリが複数存在する(たとえば Burp Suite など)ので、実際に行うのは独自CAの許可のみだ。

以降で実装手順を解説する。実際のコードはGitHubに掲載した。

github.com

1. Response bodyを表示する簡単なアプリを作る

まずは挙動が確認できないことには始まらないので、通信した結果を表示するだけの画面を作る。
ざっくり以下のようになるはずだ:

class MainActivity : AppCompatActivity() {

    companion object {

        private const val ENDPOINT = "https://example.com"

    }

    private val client = OkHttpClient()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.button)
            .setOnClickListener {
                val req = Request.Builder()
                    .url(ENDPOINT)
                    .build()

                GlobalScope.launch(Dispatchers.IO) {
                    val result = client.newCall(req).execute().let {
                        it.body()?.string() ?: it.code().toString()
                    }

                    GlobalScope.launch(Dispatchers.Main) {
                        findViewById<TextView>(R.id.text)
                            .text = result
                    }
                }
            }
    }
}

このような表示を期待する。

f:id:S64:20190424173808p:plain:w250

2. 構成ファイルを作成・配置する

今回の設定はアプリ内でのみ有効になる。試しに

  • 全ての通信において、プリインストールのCAとres/raw/myca.derを信頼する

という設定にし、src/main/res/xml/nsc.xml に配置した:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config>
        <trust-anchors>
            <certificates src="system" />
            <certificates src="@raw/myca" />
        </trust-anchors>
    </base-config>
</network-security-config>

3. CAを追加する

前項で設定したとおり、src/main/res/raw/myca.derとして追加する。

4. Manifestに設定する

nsc.xmlというファイル名自体には意味がなく、これを AndroidManifest.xml から参照する必要がある。

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    ...

    <application
            ...
            android:networkSecurityConfig="@xml/nsc">
        ...
    </application>

</manifest>

<application/> 要素に android:networkSecurityConfig で設定する。

以上で、独自証明書を用いた通信の傍受や改竄が可能になったはずだ。

`Cannot find a version of '*' that satisfies the version constraints` というエラーがでた時

Android開発で、Gradleから以下のようなエラーが出た:

> Task :app:lint FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:lint'.
> Could not resolve all artifacts for configuration ':${submodule}:debugAndroidTestRuntimeClasspath'.
   > Could not resolve com.android.support:support-annotations:28.0.0.
     Required by:
         project :${submodule}
      > Cannot find a version of 'com.android.support:support-annotations' that satisfies the version constraints: 
           Dependency path '${project}:${submodule}:unspecified' --> 'com.android.support:support-annotations:28.0.0'
           Constraint path '${project}:${submodule}:unspecified' --> 'com.android.support:support-annotations' strictly '26.1.0' because of the following reason: debugRuntimeClasspath uses version 26.1.0

これはなにかというと、マルチモジュールなプロジェクトで依存関係を組んでいる時、

  • 上位モジュールが要求するライブラリがstrictにバージョンを指定している
  • その下位のモジュールは当該ライブラリを直接利用はしていない
  • が、このモジュールの依存する他のライブラリが要求するため、間接的に依存している
    • そしてこの間接的に使われる当該ライブラリバージョンは上位のモノと一致していない
  • 結果、ライブラリバージョンがconflictしているために決定できない

という状況で発生する。


すなわちやることはシンプルで、下位モジュールにも同様に設定すればいい。
今回の場合は、上位のモジュールで28.0.0が打たれているcom.android.support:support-annotations が、下位のモジュールではバージョンがunspecifiedになっているわけなので、その依存を追加すればいい:

...
dependencies {
    implementation 'com.android.support:support-annotations:28.0.0'
}
...

Android Lintで`Unknown issue id "all"`が出る場合

Android Lintを掛けようとして、こんなエラーが出た:

Error: Unknown issue id "all", found in /${projectdir}/lint.xml [LintError]

設定ファイルの当該箇所はこれ:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    ...
    <issue id="all"> <!-- 🈁 -->
        <ignore path="..."/>
        ...
    </issue>
    ...
</lint>

使っていたAndroid Gradle Pluginはこれ:

...
buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        ...
    }
}
...

これはなにかというと、単にAndroid Gradle Pluginのバグである。 ここで報告されていた
この問題はAndroid Gradle Plugin 3.3.0で既に修正されている。

...
buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.0'
        ...
    }
}
...

ただし、このAndroid Gradle Plugin 3.3.0は要求するGradleバージョンの引き上げも含まれるため注意。

> Failed to apply plugin [id 'com.android.application']
   > Minimum supported Gradle version is 4.10.1. Current version is [currentversion]. If using the gradle wrapper, try editing the distributionUrl in /${projectdir}/gradle/wrapper/gradle-wrapper.properties to gradle-4.10.1-all.zip

具体的には、gradle-4.10.1以上にしなければいけない。

./gradlew wrapper --gradle-version=4.10.1
# 
# Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
# See https://docs.gradle.org/4.8.1/userguide/command_line_interface.html#sec:command_line_warnings
# 
# BUILD SUCCESSFUL in 5s
# 1 actionable task: 1 executed
git status
# On branch ${yourbranch}
# Your branch is ahead of 'origin/${yourbranch}' by ${num} commit.
#   (use "git push" to publish your local commits)
# 
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
# 
#  modified:   gradle/wrapper/gradle-wrapper.properties
# 
# no changes added to commit (use "git add" and/or "git commit -a")
./gradlew wrapper
# 
# Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0.
# Use '--warning-mode all' to show the individual deprecation warnings.
# See https://docs.gradle.org/4.10.1/userguide/command_line_interface.html#sec:command_line_warnings
# 
# BUILD SUCCESSFUL in 5s
# 1 actionable task: 1 executed
git status
# On branch ${yourbranch}
# Your branch is ahead of 'origin/${yourbranch}' by ${num} commit.
#   (use "git push" to publish your local commits)
# 
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
# 
#  modified:   gradle/wrapper/gradle-wrapper.jar
#  modified:   gradle/wrapper/gradle-wrapper.properties
# 
# no changes added to commit (use "git add" and/or "git commit -a")

Gitリポジトリの中身を全部一気に消す

Gitリポジトリの中のファイルを一括で消すコマンドといえば、よく言われるのはこれ:

git rm -rf .

しかしこれはちょっと問題があって、

  • GitでUntrackedなファイルは削除対象じゃない
  • 削除対象ファイルが0コの場合はエラーになる

というちょっと扱いづらいもの。


ならば当然こういう選択肢が出る:

rm -rf ./*

ただこれだと今度は.gitディレクトリまで削除されてしまう。


そこでオススメするonelinearはコチラ:

git clean -fdx && test $(git ls-files | wc -l) -eq 0 || git rm -rf .

ちょっと解説。

git clean -fdx

git clean -fdxのオプションは、

  • -f: force
  • -d: ディレクトリを対象に
  • -x: ignore対象も削除

これでまずUntrackedなファイル含めクリーンに。

&& test $(git ls-files | wc -l) -eq 0

次に test $(git ls-files | wc -l) -eq 0 では、

  • gitの管理するファイル一覧の行数をカウントし、
  • ファイル数が0ならOK、
  • そうでない場合はエラーにする

という内容。

|| git rm -rf .

最後に注目すべきは、||でつなげてあるということ。
これにより、手前のtestが失敗している(= ファイルが残っている)なら実行するし、成功している(= ファイルがもうない)なら実行されない。

よって、エラーを吐かせずにリポジトリの中身がクリーンにできちゃう。