3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】Custom Tabs を閉じる技術 〜Intent フラグの活用〜

Last updated at Posted at 2025-12-13

本記事は GENDA Advent Calendar 2025 シリーズ2 - Day 14 の記事です。

はじめに

先日、Android アプリ開発において Custom Tabs を使った認証フローを実装する機会がありました。
Custom Tabs で Web ページの認証画面を開き、認証完了後にリダイレクト(ディープリンク)でアプリに戻るという流れです。(OIDC 認可コードフロー)

今までは「Web ページで認証画面を開くのはアプリ内 WebView で良いのでは?」と個人的に考えていました。
しかし、昨今のセキュリティ事情などからアプリ内 WebView では利用できない機能が増えており、現在は Custom Tabs の利用が必要となるケースが多くなっているようです。

Google のドキュメントからも、WebView ではなく Custom Tabs の利用が推奨されていることがわかります。

Custom Tabs で認証フローの実装を進める中で、アプリへ戻る際に 「Custom Tabs を閉じる方法」 に分かりにくさを感じました。
特に普段 Android 開発をメインにしていない方からすると、直感的には理解しにくい挙動かと思います。

そこで本記事では、Custom Tabs を用いた認証フローにおいて、 いかにして Custom Tabs を閉じ、アプリに戻るか について解説します。

Custom Tabs について詳しくは以下をご確認ください。

Custom Tabs は閉じられない

まず前提として、Custom Tabs にはプログラムで明示的に閉じる API は用意されていません。
(※ もちろん、ユーザーが画面左上の「✕」ボタンを押せば閉じることはできます)

Web 側の実装として JavaScript で window.close() を呼び出すことで閉じられるケースもあります。
しかし、Web 側のコードに手を入れることができなかったり、ブラウザ側の制限で window.close() が期待通りに動作しなかったりと、確実に閉じられるとは限りません。

「閉じる」のではなく「消す」

ではどうやって Custom Tabs を閉じるのかですが、「閉じる」のではなく「消す」 というアプローチを取ります。

Custom Tabs はアプリの Activity スタック の一部として扱われます。
Activity スタックとは、アプリ内で遷移した画面(Activity)が積み重なっていく仕組みのことです。
この Activity スタックに対して Intent フラグ を適切に指定することで重なりを整理でき、その仕組みを利用して Custom Tabs を閉じる(消す)ことが可能です。

タスクとバックスタックについて詳しくは以下をご確認ください。

具体例

ここからは以下のような認証フローを例に、Custom Tabs を閉じる(消す)方法を解説します。

  1. メイン画面(MainActivity)から Custom Tabs を起動し、Web の認証画面を表示する。
  2. ユーザーが認証を完了すると、Web 側から myapp://callback のようなカスタムスキームでディープリンクが発行される。
  3. アプリ側でディープリンクを受け取り、MainActivity に戻る。

登場人物として、MainActivity の他にディープリンクの受け口として CallbackActivity を用意します。

AndroidManifest.xml
<!-- AndroidManifest.xml のイメージ -->

<activity
    android:name=".MainActivity" />

<activity
    android:name=".CallbackActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="callback" />
    </intent-filter>
</activity>

1. Custom Tabs を起動し、認証する

MainActivity で Custom Tabs を起動し、認証フローを開始します。

MainActivity.kt
fun startAuth() {
    val authUrl = "https://example.com/oauth/authorize?client_id=..."

    // Custom Tabs の Intent を作成
    val customTabsIntent = CustomTabsIntent.Builder()
        .build()

    // 認証画面を開き、認証フローを開始
    customTabsIntent.launchUrl(this, Uri.parse(authUrl))
}
+--------------------+
|  Custom Tabs (Web) | <- Custom Tabs starts
+--------------------+
+--------------------+
|    MainActivity    |
+--------------------+

2. CallbackActivity でディープリンクを受ける

認証完了後、 myapp://callback というカスタムスキームでアプリ側にディープリンクが飛んできます。これを CallbackActivity で受け取ります。

+--------------------+
|  CallbackActivity  | <- Launched by Deep Link
+--------------------+
+--------------------+
|  Custom Tabs (Web) |
+--------------------+
+--------------------+
|    MainActivity    |
+--------------------+

3. ディープリンクを受け流す

CallbackActivity で受け取ったディープリンクを元に、MainActivity へ遷移する Intent を作成します。

この Intent に対して、以下のフラグを組み合わせることが重要なポイントとなります。

  • Intent.FLAG_ACTIVITY_CLEAR_TOP:
    • スタック内で対象の Activity(今回の場合 MainActivity)より上にある Activity をすべて破棄します。これにより、上に乗っていた Custom Tabs が消えます
  • Intent.FLAG_ACTIVITY_SINGLE_TOP:
    • 対象の Activity が既にスタックの先頭に存在する場合、 Activity を再生成せず、既存のインスタンスを使い回します。これにより、認証フローを継続できます。
CallbackActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    val targetIntent = Intent(this, MainActivity::class.java).apply {
        // 現在のスタックにある MainActivity より上の Activity(Custom Tabs含む)を消す
        flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
        // 認証データなどを引き継ぐ場合
        data = intent.data 
    }
    startActivity(targetIntent)
    finish() // CallbackActivity 自身も終了(明示的に呼ばなくても消える)
}
+--------------------+          + - - - - - - - - - -+
|  CallbackActivity  | ---+       CallbackActivity     x Destroyed
+--------------------+    |     + - - - - - - - - - -+
+--------------------+    |     + - - - - - - - - - -+
|  Custom Tabs (Web) |    | ==>   Custom Tabs (Web)    x Destroyed
+--------------------+    |     + - - - - - - - - - -+
+--------------------+    |     +--------------------+
|    MainActivity    | <--+     |    MainActivity    | <- Remains
+--------------------+          +--------------------+

4. MainActivity で処理を受け取る

結果として、呼び出し元の MainActivity に戻り、認証フローを続行できます。

MainActivity.kt
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)

    // CallbackActivity から渡されたデータ(認証コードなど)を取り出す
    intent.data?.let { uri ->
        Log.d("Auth", "Callback received: $uri")
        
        // 認証フローを続行
        // ...
    }
}
+--------------------+
|    MainActivity    |
+--------------------+

認証系ライブラリを見てみる

認証系ライブラリでも、同様のアプローチが取られていることを確認できます。
本記事の CallbackActivity に相当する部分を抜粋しておきます。

  • AppAuth-Android : RedirectUriReceiverActivity

  • Auth0.Android : RedirectActivity

Intent フラグの仕組みを理解した上でこれらのライブラリのコードを読むと、理解が捗るのではないでしょうか。

まとめ

本記事では、Custom Tabs を用いた認証フローにおいて、どのようにして Custom Tabs を閉じ、アプリに戻るかについて解説しました。

Intent フラグを活用し Activity スタックを操作することで Custom Tabs を制御できます。ややテクニカルに思えるかもしれませんが、Android 開発においてはなくてはならない技術のひとつです。

この記事の内容が、どこかの誰かの役に立てば幸いです。

【付録】 Auth Tab もあります

最後に Auth Tab についても触れておきます。

簡単に言うと認証に特化した Custom Tabs であり、これを使うと自動でタブを閉じることが可能です。

下記の例では myapp スキームで始まるリダイレクト URI を受け取った際に自動でタブが閉じられ、 handleAuthResult に認証結果が渡されます。

private val launcher = AuthTabIntent.registerActivityResultLauncher(this, this::handleAuthResult)
private fun handleAuthResult(result: AuthTabIntent.AuthResult) {
    // result に認証結果が入る
}

...

val authTabIntent = AuthTabIntent.Builder().build()
authTabIntent.launch(launcher, url.toUri(), "myapp") // "myapp" はリダイレクト URI のスキーム

Auth Tab を使えば、本記事で解説したような Activity スタックの操作は不要になります。

しかし、Auth Tab は Chrome ブラウザのバージョン 137 以降でサポートされており、それ未満の場合は使用できません。また、他のブラウザアプリをメインで使っている場合も Auth Tab が利用できないケースがあるため、注意が必要です。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?