Android: アプリの Task と Activity Stack の状態をログに表示する


Andorid アプリでは Activity は Task に所属し、Activity により画面遷移すると Activity の履歴が Stack に積まれています。

https://developer.android.com/guide/components/activities/tasks-and-back-stack

ActivityManager から自身のアプリが管理する Task の情報を取得することができます。自身のアプリの Task の情報をログに出力する方法を解説します。

# 前提

* Android 5.0 以上であること

# 実装

以下の printActivityStack() Extension を実装して使用します。

```kotlin
/**
 * Context に所属する Task の Activity Stack をログに表示する
 */
fun Context.printActivityStack() {
    fun ComponentName.shortName(): String {
        // パッケージ名を省略してクラス名だけを取り出す
        return className.split(".").last()
    }
    val activityManager = checkNotNull(getSystemService<ActivityManager>())
    val tasks = activityManager.appTasks
    tasks.forEach { task ->
        val taskInfo = task.taskInfo
        val activitiesString = when (taskInfo.numActivities) {
            0 -> "[]"
            1 -> "[${taskInfo.baseActivity?.shortName()}]"
            2 -> "[${taskInfo.baseActivity?.shortName()} | ${taskInfo.topActivity?.shortName()}]"
            else -> "[${taskInfo.baseActivity?.shortName()} ... ${taskInfo.topActivity?.shortName()}].size = ${taskInfo.numActivities}"
        }
        val taskId = if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) {
            "[${taskInfo.taskId}]"
        } else ""
        Log.d("printActivityStack", "Task${taskId}: $activitiesString")
    }
}
```

# 出力

context.printActivityStack() を実行すると以下のようなログが出力されます。

Task に積まれた Activity が 1つだけのとき。

```
Task[12345]: [Feature1Activity]
```

Task に積まれた Activity が 2つだけのとき。

```
Task[12345]: [Feature1Activity | Feature2Activity]
```

Task に積まれた Activity が 3つ以上のとき。Task の最初の Activity と、最後の　Activity (最前面に表示されている Activity) の情報だけが表示されます。

```
Task[12345]: [Feature1Activity ... Feature3Activity].size = 3
```

# 解説

[ActivityManager#getAppTasks()](https://developer.android.com/reference/kotlin/android/app/ActivityManager#getapptasks)
 により、自身のアプリに所属する Task の情報を取得可能です。自身のアプリの情報であるためパーミッションは必要ありません。

[AppTask#getTaskInfo()](https://developer.android.com/reference/kotlin/android/app/ActivityManager.AppTask#getTaskInfo()) で [RecentTaskInfo](https://developer.android.com/reference/kotlin/android/app/ActivityManager.RecentTaskInfo) を取り出すと、以下の情報を参照できます。

## [TaskInfo](https://developer.android.com/reference/kotlin/android/app/TaskInfo) の情報

| 項目 | 値 |
| --- | --- |
| baseActivity | タスクの先頭の Activity |
| baseIntent | タスクを起動した Intent |
| isRunning | 起動中の Activity があるか |
| numActivities | タスクに含まれる Activity の数 |
| origActivity | Activity Alias でタスクが起動されたときの target Activity |
| taskDescription | [Activity.setTaskDescription](https://developer.android.com/reference/kotlin/android/app/Activity#settaskdescription) により設定された Task の表示名 |
| taskId | タスク ID |
| topActivity | タスクの最後の Activity (画面表示される Activity) |

Stack の情報としては baseActivity, topActivity, numActivities が該当します。先頭の Activity と末尾の Activity は取得できますが、その間にある Activity の情報は取得できません。

# Tips

## adb コマンドから Activity の情報を取得する

アプリから Task の Activity Stack に積まれたすべての Activity の情報を取得することはできませんが、adb コマンドを実行することで Stack 内のすべての Activity の情報を詳細に確認することが可能です。

https://developer.android.com/studio/command-line/dumpsys

```shell
# 以下のコマンドで ACTIVITY MANAGER ACTIVITIES を探す
# Hist #{N}: が Activity Stack 内の Activity
% adb shell dumpsys activity
...
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
  RootTask #27437: type=standard mode=fullscreen
  isSleeping=false
  mBounds=Rect(0, 0 - 0, 0)
  mCreatedByOrganizer=false
    mResumedActivity: ActivityRecord{1d10ba8 u0 com.android.chrome/com.google.android.apps.chrome.Main t27437}
    * Task{60c77cb #27437 type=standard A=10285:com.android.chrome U=0 visible=true mode=fullscreen translucent=false sz=1}
      mBounds=Rect(0, 0 - 0, 0)
      mMinWidth=-1 mMinHeight=-1
      userId=0 effectiveUid=u0a285 mCallingUid=u0a321 mUserSetupComplete=true mCallingPackage=com.sonymobile.launcher mCallingFeatureId=null
      affinity=10285:com.android.chrome
      intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.android.chrome/com.google.android.apps.chrome.Main}
      origActivity=com.android.chrome/com.google.android.apps.chrome.Main
      mActivityComponent=com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
      autoRemoveRecents=false isPersistable=true activityType=1
      rootWasReset=true mNeverRelinquishIdentity=true mReuseTask=false mLockTaskAuth=LOCK_TASK_AUTH_PINNABLE
      Activities=[ActivityRecord{1d10ba8 u0 com.android.chrome/com.google.android.apps.chrome.Main t27437}]
      askedCompatMode=false inRecents=true isAvailable=true
      mRootProcess=ProcessRecord{27a429e 7746:com.android.chrome/u0a285}
      taskId=27437 rootTaskId=27437
      hasChildPipActivity=false
      mHasBeenVisible=true
      mResizeMode=RESIZE_MODE_RESIZEABLE mSupportsPictureInPicture=true isResizeable=true
      lastActiveTime=520060 (inactive for 7s)
        Hist #0: ActivityRecord{1d10ba8 u0 com.android.chrome/com.google.android.apps.chrome.Main t27437}
          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.android.chrome/com.google.android.apps.chrome.Main bnds=[225,2237][435,2403] }
          ProcessRecord{27a429e 7746:com.android.chrome/u0a285}
...
```