JCenter / Bintray等が停止することで起きる影響を考える

2021/02/09追記:

UPDATE: To better support the community in this migration, JFrog has extended the JCenter new package versions submission deadline through March 31st 2021.

To clarify, the JCenter repository will keep serving packages for 12 months until February 1st 2022. Only the JCenter REST API and UI will be sunsetted on May 1st 2021.

本日上記のとおりアップデートがあり、

  • 新しいバージョンの公開は約1ヶ月延長され、3月末日まで可能に
  • 5月1日に廃止されるのはAPIやUIのみ、ホスティングは2022年2月1日(約1年間)までと明記

と告知されました。本記事に記されている一部の日付が変わってくる為、ご留意ください。


JFrogが2021-02-04に発表した内容。Bintray, JCenter, GoCenter, ChartCenterが終了する。

jfrog.com

詳細は1次ソースを参照してもらうほうがよいので、ここで細かい内容には触れない。
この記事は上記を既に閲覧していることを前提に、ではどのような影響を受けうるか、どのような対応が必要になるか、を現時点で想像可能な範囲で書いていく。
筆者はAndroidアプリ開発を主としている為、想像の利く範囲がJVMアプリケーションの知識以上になりづらいことをご容赦頂きたい。

何かしら救済措置やそれに相当するサービスが登場して、この記事に記す内容がハズレになってくれることを願う。

ところでurlで英語間違えててはずかしい

影響をうけるエンドユーザとその対応

冒頭のサービスへの参照を直接記述しているプロジェクト(のビルド構成ファイル)のみ、と言いたいところだが、少し恐怖心を煽るならもっと広く「JVMアプリケーションを始めとしたMavenインフラを利用している全てのプロジェクト」としておいて、まずは確認することを促したい。

例えばJCenterに存在しMaven Centralに存在しないライブラリには、

  • いくつかの広告SDK
    • MoPub Android SDK
    • Tapjoy Android SDK
    • AppLovin Android SDK
    • ほかいろいろ
  • JetBrainsの提供するごく一部のライブラリ
  • Groupieなど個人で開発されている多くのOSS

など、広く使われつつも限定目的であるものならかなりある。
また前述の「Bintray上で独自にホストされたライブラリ」であれば、

  • ExoPlayer (bintray.com/google/exoplayer)
  • OpenTok (bintray.com/tokbox/maven)

などもある。

Gradleを利用して依存関係解決をしているアプリケーション(Android向けビルドのあるアプリなら確実にそうだろう)の場合、Gradleのビルド設定記述内に下記のようなブロックがあるはず。

repositories {
    google()
    mavenCentral()
    jcenter()
    maven {
        url 'http://tokbox.bintray.com/maven'
    }
}

この記述の場合、依存関係は Google Maven Repository -> Maven Central -> JCenter -> Bintray上の独自リポジトリ(この場合はOpenTok) という順で解決が試みられる

ライブラリの中にはMaven CentralとJCenterの両方に存在するものもある為、そのような場合は影響を受けないだろう。
しかし上記のとおりJCenterにしか存在しないライブラリも存在するし、一番下にあるようなBintray上で独自にホストされたMavenリポジトリでしか配信されていないものもプロプライエタリなライブラリでは少なくない。

このようなライブラリの依存関係を持っている場合、

  • まずはそのライブラリの配布サイト(GitHubリポジトリなど)を確認する
  • 配布するMavenリポジトリをどこに変更するかアナウンスを見つける
  • ビルドスクリプトをそれに従い変更する

という作業を "該当する全てのライブラリに対して" 行う必要が出てくる可能性がある。

JCenterからの移転先としてMaven Centralが選ばれる可能性もあるので、実際の変更作業自体はそう多くないと予想している。が、少なくとも各ライブラリとそのメンテナがどんな動きを取るのか確認しておくべきだとは言えるだろう。

間接的な影響

これは可能性としてだが、自分達の使っているライブラリがさらに他のライブラリに依存している場合、そういった遠い依存ライブラリのMavenリポジトリが変更されることも当然ある。

たとえばExoPlayerはAndroidにおいてデファクトスタンダードとなっているメディア再生ライブラリであり、これに依存したライブラリもいくらか存在する。もしこれが独自にホスティングされたMavenリポジトリや GitHub Packagesのような名前空間の分割されたMavenリポジトリに移転した場合、当然自分達もそれを追加する必要が出てくるはずだ。

余談: 出荷済バイナリは影響を受けるか

多くのアプリケーションは事前に依存関係を全てパックした状態で配布される為、既に出荷済のバイナリであれば影響を受けないだろう。
一切メンテナンスを行わないのであればいいが、次にメンテナンスを行うのがJCenter終了後だった場合にはビルドができなくなっているはずだ。

ライブラリ配布者の影響

事業としてライブラリを開発し配布することもしばしばあるが、当然こういった場合にもエンドユーザと同じような対応をまずは取らなければならない。使用している各ライブラリの動向をチェックすることだ。

そして自分達もまたそれを配布している場合、下記のような作業が入ってくるだろう:

  • 自分たちがJCenterやBintrayを利用している場合、できるだけ早く移転作業を行う
    • 今回は5月1日がデッドラインであり、これ以降ライブラリが取得不能になる為
    • また2月28日には新しいバージョンを受け付けなくなる為、アップデート配信の都合上決定は急ぐ必要がある
  • 顧客向けに、リポジトリの列挙内容として不要になるもの、新たに追加してもらう必要があるものをできるだけ迅速に伝える
    • 間接的に依存するライブラリがあった場合、それ用の新たなリポジトリを追加しないと依存関係が正常に解決できない場合がある
    • 連絡が遅れた場合、顧客側スケジュールにおける動作確認予定等の都合で 必要なアップデートが提供できない可能性がある

自分達の製品が依存するJFrog系リポジトリ経由のライブラリが多いほど 顧客へ依頼する変更が多くなる可能性があることを考慮して進める必要があるが、どうしても依存先の方針決定を待つ必要もあるのが難しい。1回でまとめて連絡するのは難しくなるかもしれない。

塩漬けされた顧客プロジェクトの場合

一切メンテナンスを行っておらず、出荷済みのバイナリのみでビジネスを行っている顧客もまま居るだろう。このような顧客は強く影響を受けるわけではない。

ただし、当然自分達の更新や依存先の更新を反映してもらう為には改めて依存関係解決が実行してもらう必要が出てくるだろうし、その時には今回の対応を行ってもらう必要がある。
可能な限りは非アクティブな開発者にも対応を促しておいたほうがよいと考える。

追記: 最悪のパターン

どうにもならないパターンも発生しうるなと思った為、これは別記事に書いた。

h.s64.jp

onActivityResultはFramework APIとAndroidXで挙動が異なる

その前に

現在は startActivityForResult / onActivityResult の代わりに Activity Result API というAndroidXライブラリが用意されており、これら従来のAPIは androidx.activity の最新alpha版で既にdeprecatedになっています。
そのため通常のアプリにおいては、本記事で取り扱うAPIは利用すべきではありません。恐らく、レガシーな開発環境を考慮に入れたライブラリ開発等でのみ必要なナレッジになるかと思います。


Android開発において画面間での結果受け渡しを行う方法は複数思い当たるが、最もオーソドックスかつあらゆるライフサイクル都合に対応可能なのが startActivityForResult / onActivityResult を用いた方法になる。
このAPIはActivityだけでなくFragment側にも存在し、Fragmentから他Activityを呼び出し、その結果を呼び出し元のFragmentで受け取る、なんていうことも可能だ。

ご存知のとおりAndroidは歴史的理由で "Fragment" と呼ばれる実装が2通り存在し、これは継承関係を持っていない。ここでは便宜上、

  • Android SDKに含まれるFragment実装(非推奨になっている)を "Framework API" と呼ぶ
  • AndroidX (Support Library) に含まれるFragment実装(通常こちらを使う)を "AndroidX" と呼ぶ

としていく。

Framework APIでの挙動

検証用アプリを用意した。

github.com

ざっくり、下記のような実装になっている:

  • Activity内にFramework API側のFragment (PlatformLauncherとする) を用意する
  • 上記Fragmentから startActivityForResult を叩き、次のActivityを表示する
  • 次のActivityは何かしらのresultをsetし、finishする

これを実行すると、PlatformLauncher#onActivityResult に結果が返る。呼び出し元はFragmentなので、この親Fragmentには特段通知が行かない。直感に反しない純粋な挙動だ。

Image from Gyazo

AndroidXでの挙動

上記検証用アプリ内のもうひとつのボタンは、このFragmentをAndroidXのFragment実装(SupportLauncherとする)に変更している。
この状態で実行すると、当然期待どおり SupportLauncher#onActivityResult に結果が返るのだが、さらにこの親のActivityの onActivityResult にも結果が返るのである。

Image from Gyazo

直感に反するどころか、そもそもFramework APIと異なる挙動になる。それに、親Activity側には自分の指定した値と全く異なるrequestCodeが返っている。
これは何か。

Framework APIの実装

Framework側のresult実装は(あくまでイメージなので厳格性に欠けるが)ざっくり下記のような仕様になっている:

  • Fragmentの startActivityForResult が実行されると、Fragmentの持つ who という値と共に親Activityまでリクエストを転送する
  • 親Activityはwhoの値と共に、Activity表示リクエストをdispatchする
  • resultの設定されたActivityが終了した際、whoの値を用いて呼び出し元を検索し該当するインスタンス(Fragment)にだけ通知をする
// Activity.java

@Deprecated
public void startActivityFromFragment(@NonNull Fragment fragment,
        @RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
    startActivityForResult(fragment.mWho, intent, requestCode, options);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5859-L5886
// Activity.java

@Override
@UnsupportedAppUsage
public void startActivityForResult(
        String who, Intent intent, int requestCode, @Nullable Bundle options) {
    Uri referrer = onProvideReferrer();
    if (referrer != null) {
        intent.putExtra(Intent.EXTRA_REFERRER, referrer);
    }
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, who,
            intent, requestCode, options);
    if (ar != null) {
        mMainThread.sendActivityResult(
            mToken, who, requestCode,
            ar.getResultCode(), ar.getResultData());
    }
    cancelInputsAndStartExitTransition(options);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L5897-L5919
// Activity.java

@UnsupportedAppUsage
void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data,
        String reason) {
    if (false) Log.v(
        TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode
        + ", resCode=" + resultCode + ", data=" + data);
    mFragments.noteStateNotSaved();
    if (who == null) {
        onActivityResult(requestCode, resultCode, data);
    } else if (who.startsWith(REQUEST_PERMISSIONS_WHO_PREFIX)) {
        who = who.substring(REQUEST_PERMISSIONS_WHO_PREFIX.length());
        if (TextUtils.isEmpty(who)) {
            dispatchRequestPermissionsResult(requestCode, data);
        } else {
            Fragment frag = mFragments.findFragmentByWho(who);
            if (frag != null) {
                dispatchRequestPermissionsResultToFragment(requestCode, data, frag);
            }
        }
    } else if (who.startsWith("@android:view:")) {
        ArrayList<ViewRootImpl> views = WindowManagerGlobal.getInstance().getRootViews(
                getActivityToken());
        for (ViewRootImpl viewRoot : views) {
            if (viewRoot.getView() != null
                    && viewRoot.getView().dispatchActivityResult(
                            who, requestCode, resultCode, data)) {
                return;
            }
        }
    } else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
        Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
        getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus());
    } else {
        Fragment frag = mFragments.findFragmentByWho(who);
        if (frag != null) {
            frag.onActivityResult(requestCode, resultCode, data);
        }
    }
    writeEventLog(LOG_AM_ON_ACTIVITY_RESULT_CALLED, reason);
}

// https://github.com/aosp-mirror/platform_frameworks_base/blob/a4ddee215e41ea232340c14ef92d6e9f290e5174/core/java/android/app/Activity.java#L8123-L8162

AndroidX側の実装

対してAndroidX側実装だが、そもそもSupport LibraryとしてFragmentの仕組みが利用できない(Platformに含まれない)OSでの利用を想定して作られたものであるため、who値と共に直接dispatchしこれを元に手繰るような手段は取れない。そのためこちらでは、

  • Fragmentの startActivityForResult が実行されると、Fragmentのインスタンス共に親Activityまでリクエストを転送する
  • 親まで来たら、ActivityからstartActivityForResultを実行する。この際requestCodeの入替をしており、上位16bitをfragmentのindex、下位16bitを実際のrequestCodeとした値になる。
  • result設定したActivityが終了したら、当然上記の親Activityにまず通知が行く。この中で上位16bitを取り出しFragmentを手繰り、一致するものにresult通知する

というような感じの実装になっている。

// FragmentActivity.java

public void startActivityFromFragment(@NonNull Fragment fragment,
        @SuppressLint("UnknownNullness") Intent intent, int requestCode,
        @Nullable Bundle options) {
    mStartedActivityFromFragment = true;
    try {
        if (requestCode == -1) {
            ActivityCompat.startActivityForResult(this, intent, -1, options);
            return;
        }
        checkForValidRequestCode(requestCode);
        int requestIndex = allocateRequestIndex(fragment);
        ActivityCompat.startActivityForResult(
                this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options);
    } finally {
        mStartedActivityFromFragment = false;
    }
}

// https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L781-L800
// FragmentActivity.java

@Override
@CallSuper
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    mFragments.noteStateNotSaved();
    int requestIndex = requestCode>>16;
    if (requestIndex != 0) {
        requestIndex--;


        String who = mPendingFragmentActivityResults.get(requestIndex);
        mPendingFragmentActivityResults.remove(requestIndex);
        if (who == null) {
            Log.w(TAG, "Activity result delivered for unknown Fragment.");
            return;
        }
        Fragment targetFragment = mFragments.findFragmentByWho(who);
        if (targetFragment == null) {
            Log.w(TAG, "Activity result no fragment exists for who: " + who);
        } else {
            targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data);
        }
        return;
    }
    ActivityCompat.PermissionCompatDelegate delegate =
            ActivityCompat.getPermissionCompatDelegate();
    if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) {
        // Delegate has handled the activity result
        return;
    }


    super.onActivityResult(requestCode, resultCode, data);
}

// https://github.com/aosp-mirror/platform_frameworks_support/blob/b9cd83371e928380610719dfbf97c87c58e80916/fragment/fragment/src/main/java/androidx/fragment/app/FragmentActivity.java#L149-L182

このとおり、Activity / Fragment両側にresultが通知され、Framework側とは異なる挙動を示すのにはある側面では致し方ない事由が存在するのである。

影響

この仕様によって特段影響があるかというと、多くの場合は影響がない。
AndroidのActivityは、異なるアプリケーションのActivity間(異なるプロセス間)でも相互に連携し動作できる設計思想にある。そのためIntentやrequestCodeとして未知のものが通知される可能性を考慮した実装を行うことを想定している。
事実として(現在は新しいAPIに置き換わったため参照できないが)Android Developersでは自身が処理可能なrequestCodeだけ処理対象にするコードが例示されている。

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    // Check which request we're responding to
    if (requestCode == PICK_CONTACT_REQUEST) {
        // Make sure the request was successful
        if (resultCode == Activity.RESULT_OK) {
            // The user picked a contact.
            // The Intent's data Uri identifies which contact was selected.

            // Do something with the contact here (bigger example below)
        }
    }
}

// https://web.archive.org/web/20190703204900if_/https://developer.android.com/training/basics/intents/result?hl=ja#ReceiveResult

すなわち、AndroidX側の親Activityに本来Fragment側で取りたいrequestCode以外が到達していてもこれは無視すればよいし、そのような実装をしている限りFramework API側の実装に切り替わるようなことがあっても問題なく動作するはずだ。

MoPubのLifecycle Callbacksはメディエーションを使うなら実装したほうがよい

MoPubのRewarded Video実装をする時など、ドキュメントを読んでいると下記のような記述に遭遇する:

Step 5. Optionally Implement Lifecycle Callbacks

Network SDKs need to be notified of changes in the activity lifecycle so they can keep track of the current activity and load and show rewarded videos. For example, you can add lifecycle callbacks to all of your activities (including your Launcher Activity) that will load or show rewarded videos.

For Mediation SDKs that track Activity lifecycle events, you must notify MoPub when those events happen, so that MoPub can pass them onto mediated network SDKs, as shown:

developers.mopub.com

MoPubの提供する Lifecycle Callbacks と称するAPI郡に関する記述なのだが、 "Optionally" と書いてあるし、実装しなくても動作する場合があるし、何より複数に渡る画面へこれを埋め込むのは現実的ではない。そんなわけで無視して進めてしまいそうになる。
しかし "Network SDKs need to be notified of changes in the activity lifecycle" とあるように、実はメディエーション機能を使う場合には実態として必須と言って過言ではない。

Custom SDK Network側ではどのように見えてくるか

今回はリワード広告を例に取る。
前提として、MoPubでリワードを表示するには下記のような手順を取る:

// MyActivity.kt

val conf = SdkConfiguration.Builder(adUnitId)
    .withAdditionalNetwork(MyAdapterConfiguration::class.java.name)
    .withLogLevel(MoPubLog.LogLevel.DEBUG)
    .build()

MoPub.initializeSdk(this, conf, listener)

MoPubRewardedVideos.loadRewardedVideo(adUnitId, MyMediationSettings())

MoPubRewardedVideos.showRewardedVideo(adUnitId)

この要求を受け、メディエーション先アドネットワークのSDKは下記のようなコールバックを受ける:

// MyRewardedVideoAdapter.kt

override fun checkAndInitializeSdk(launcherActivity: Activity, adData: AdData): Boolean {
    // do something
    return true
}

override fun load(context: Context, adData: AdData) {
    // do something
    mLoadListener.onAdLoaded()
}

override fun show() {
    // do something
    mInteractionListener.onAdShown()
    mInteractionListener.onAdImpression()

    MoPubReward.success(MoPubReward.NO_REWARD_LABEL, MoPubReward.DEFAULT_REWARD_AMOUNT).let {
        mInteractionListener.onAdComplete(it)
    }

    mInteractionListener.onAdDismissed()
}

これらの詳細な実装は下記を参考にしてほしい。

github.com

loadRewardedVideo の時にはContextを渡していないが、Custom Event側はContextを受け取っている。これは何か?
MoPubの実装を辿ると、これが MoPub::initializeSdk の際に渡したものであることがわかる:

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177
// MoPubRewardedVideoManager.java

final AdAdapter adAdapter = (AdAdapter) adAdapterConstructor.newInstance(
        sInstance.mMainActivity.get(),
        baseAdClassName,
        adDataBuilder.build()
);

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L600-L604

そしてこの値は最初のinit時に与えられたものを保持し続けていることも確認できる。(これを書き換えるのがLifecycle Callbacksである。後述)

// MoPubRewardedVideoManager.java

public static synchronized void init(@NonNull Activity mainActivity, MediationSettings... mediationSettings) {
    if (sInstance == null) {
        sInstance = new MoPubRewardedVideoManager(mainActivity, mediationSettings);
    } else {
        MoPubLog.log(CUSTOM, "Tried to call initializeRewardedVideo more than once. Only the first " +
                "initialization call has any effect.");
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L170-L177

すなわち最初に与えられたActivityを保持しつづけ、以後Contextが必要になった際はこれを使い回す。これらのコードを字面だけで読み取るなら、MoPubは下記のようなアプリのみを想定した仕様であることになる:*1

  • シングルアクティビティ・マルチフラグメント等、Activityインスタンスがアプリケーション全体を通して共通であるアプリ
  • Activity利用数はともかく、広告の初期化〜読込〜表示をするポイントが単一であるアプリ

問題が起きうるパターン

しかし上記には問題があり、たとえば下記のようなパターンにおいては当然ながら問題を引き起こしうる:

  • Splash表示をするActivityがあり、ここでinitializeを行う。この画面は数秒後にfinishされる。
  • たとえばショップ画面、たとえばゲームのリザルト画面など、異なる複数種類のActivityから動画リワードが繋ぎこまれる
  • メディエーション先SDKが開放されていない(= 通常の)Contextを用いて何かしらの処理を行うため、解放済みのContextでは広告表示に失敗する仕様になっている。

Android F/Wでは開放されていないContextのインスタンスがないと利用できないAPIが多数存在し、また動画リワードのような商材においては表示元となるActivityとの関係性が正しく定義されていることが期待されるため、MoPubのAPI仕様では対応できないことになる。

Lifecycle Callbacksは何をしているか

上記のようなパターンへの対処手段としてMoPubが用意しているのが Lifecycle Callbacks と呼ばれるAPI郡で、これはinit時に握ったActivityインスタンスを入れ替える処理が実行されるものだ。

// MoPub.java

public static void onCreate(@NonNull final Activity activity) {
    MoPubLifecycleManager.getInstance(activity).onCreate(activity);
    updateActivity(activity);
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L306-L309
// MoPub.java

try {
    Class moPubRewardedVideoManagerClass = Class.forName(
            MOPUB_REWARDED_VIDEO_MANAGER);
    sUpdateActivityMethod = Reflection.getDeclaredMethodWithTraversal(
            moPubRewardedVideoManagerClass, "updateActivity", Activity.class);
} catch (ClassNotFoundException e) {

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-base/src/main/java/com/mopub/common/MoPub.java#L412-L417
// MoPubRewardedVideoManager.java

@ReflectionTarget
public static void updateActivity(@Nullable Activity activity) {
    if (sInstance != null) {
        sInstance.mMainActivity = new WeakReference<>(activity);
    } else {
        logErrorNotInitialized();
    }
}

// https://github.com/mopub/mopub-android-sdk/blob/3a7bd85201cb9cc7c91b14505e2ba3d16bd67393/mopub-sdk/mopub-sdk-fullscreen/src/main/java/com/mopub/mobileads/MoPubRewardedVideoManager.java#L179-L186

これにより load の際に渡されるContextが入れ替わるため、呼び出し元画面のContextを要求する多くのSDKは初めて期待どおりの挙動を実現可能になる。 load にはContextが渡されるのに対し show ではContextが渡されない問題*2については、Custom Event側で getLifecycleListener を実装することでなんとか回避することも可能だろう。

まとめ

  • 本来MoPubは複数Activityを跨いだ利用を考慮したAPI設計ではない
  • MoPubの提供するLifecycle Callbacksは、メディエーションを利用する限り事実上の必須実装に等しい
  • Custom Eventを実装する場合、期待するContextが常に渡らないことを考慮に入れて設計する必要がある

*1:正直に言って、Android F/W事情に沿っていない至極ナンセンスな仕様である

*2:本当に中途半端である

AndroidでServiceを使っていると、タスク削除してもプロセスは生き残る場合がある

先日、お客様のとこで直感に反する挙動に遭遇したためメモ。気付いてしまえば当たり前だが、少々想像力が必要だった。

Androidには最近使ったアプリ(Recents)という機能があり、ここで他のアプリに戻ったり、現在バックグラウンドで保持されているアプリのタスクを削除することができる。
通常、タスク削除されたアプリはプロセスが終了する。

ごく簡単な実装をしてみた。下記のような、Applicationクラスでフラグを管理し、メインの画面が作成された回数を表示するもの。

class MyApp : Application() {

    var count = 0

}
class MainActivity : AppCompatActivity() {

    private val textView by lazy { findViewById<TextView>(R.id.text) }
    private val myApp by lazy { application as MyApp }

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

        myApp.count++

        textView.text = "MyApp#count == ${myApp.count}"
    }
}

何も難しいことはない。ご想像どおりタスクを削除する度に新しいプロセスが作成され、下記のような挙動になる。

この時点の実装は下記のとおり。

github.com


では、下記のようにForeground ServiceをApplicationクラスで立ち上げてみる。

class MyApp : Application() {

    var count = 0

    // 新たに下記を実装
    override fun onCreate() {
        super.onCreate()
        startForegroundService(Intent(this, MyService::class.java))
    }

}

Serviceの中身は特段何も無い。

class MyService : Service() {

    override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

}

するとどうだろう。タスク削除後にアプリを起動し直しても、カウントは上がり続ける。

github.com


ここまでの実装過程を見ていれば、何が起きているかは一目瞭然。タスクとして保持すべきものが存在する場合、プロセスは当然同じものが利用されることになるわけだ。
すなわち、Activityのcloseは必ずしも onBackPressed でブロックしていれば完璧ではない。Activityが意図しない形でcloseされかつ、プロセスが終了していない、というパターンが存在することになる。

このようなパターンを考慮して実装するには、下記のような方法が思いつく:

  1. Serviceに android:stopWithTask="true" を設定する
  2. Service#onTaskRemoved を実装し、自身を終了ないし適切に処理する
  3. Activity自身のonDestroyで適切に後処理をする
  4. Application#registerActivityLifecycleCallbacksonActivityStopped を監視する

通常は1の実装で十分だろう。ユーザが明示的にタスクの終了を求めている場合、当該アプリの処理はすべて停止している状態が理想的だ。
実際に、Spotifyなどの著名なアプリのほとんどは音楽が再生中だった場合もユーザの手でタスクが削除された場合はForeground Serviceが停止する仕様となっている。

結局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 -> プロジェクトの設定 -> クラウドメッセージング -> プロジェクト認証情報 から取得できるので、これを用いて変更作業をする。