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側の実装に切り替わるようなことがあっても問題なく動作するはずだ。