Skip to content

Commit d987c0a

Browse files
committed
Add ActivityTaskTracker and ScrollViewsExtension for test
1 parent 4ee0cb8 commit d987c0a

File tree

9 files changed

+809
-1
lines changed

9 files changed

+809
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package me.ycdev.android.lib.common.activity
2+
3+
import android.content.ComponentName
4+
5+
data class ActivityInfo(
6+
val componentName: ComponentName,
7+
val taskId: Int,
8+
var state: State = State.None
9+
) {
10+
fun makeCopy(): ActivityInfo {
11+
val cloned = ActivityInfo(componentName, taskId)
12+
cloned.state = state
13+
return cloned
14+
}
15+
16+
enum class State {
17+
None,
18+
Created,
19+
Started,
20+
Resumed,
21+
Paused,
22+
Stopped,
23+
Destroyed
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package me.ycdev.android.lib.common.activity
2+
3+
import android.content.ComponentName
4+
import java.util.Stack
5+
6+
class ActivityTask(val taskId: Int) {
7+
private val activities = arrayListOf<ActivityInfo>()
8+
9+
internal fun addActivity(activity: ActivityInfo) {
10+
if (activity.taskId != taskId) {
11+
throw RuntimeException("Activity taskId[${activity.taskId}] != AppTask[$taskId]")
12+
}
13+
activities.add(activity)
14+
}
15+
16+
internal fun popActivity(componentName: ComponentName): ActivityInfo {
17+
val it = activities.asReversed().iterator()
18+
while (it.hasNext()) {
19+
val activity = it.next()
20+
if (activity.componentName == componentName) {
21+
it.remove()
22+
return activity
23+
}
24+
}
25+
throw RuntimeException("Cannot find $componentName")
26+
}
27+
28+
fun lastActivity(componentName: ComponentName): ActivityInfo {
29+
activities.asReversed().forEach {
30+
if (it.componentName == componentName) {
31+
return it
32+
}
33+
}
34+
throw RuntimeException("Cannot find $componentName")
35+
}
36+
37+
fun topActivity(): ActivityInfo {
38+
if (activities.isEmpty()) {
39+
throw RuntimeException("The task is empty. Cannot get the top Activity.")
40+
}
41+
return activities[activities.lastIndex]
42+
}
43+
44+
/**
45+
* @return The last Activity in returned list is the top Activity
46+
*/
47+
fun getActivityStack(): Stack<ActivityInfo> {
48+
val stack = Stack<ActivityInfo>()
49+
activities.forEach {
50+
stack.push(it)
51+
}
52+
return stack
53+
}
54+
55+
fun isEmpty() = activities.isEmpty()
56+
57+
fun makeCopy(): ActivityTask {
58+
val task = ActivityTask(taskId)
59+
activities.forEach {
60+
task.activities.add(it.makeCopy())
61+
}
62+
return task
63+
}
64+
65+
override fun toString(): String {
66+
return "AppTask[taskId=$taskId, activities=$activities]"
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package me.ycdev.android.lib.common.activity
2+
3+
import android.app.Activity
4+
import android.app.Application
5+
import android.os.Bundle
6+
import androidx.annotation.GuardedBy
7+
import androidx.annotation.VisibleForTesting
8+
import timber.log.Timber
9+
10+
object ActivityTaskTracker {
11+
private const val TAG = "ActivityTaskTracker"
12+
13+
internal val lifecycleCallback = MyLifecycleCallback()
14+
15+
private val allTasks: HashMap<Int, ActivityTask> = hashMapOf()
16+
private var focusedTaskId: Int = -1
17+
private var resumedActivity: Activity? = null
18+
19+
private var debugLog: Boolean = false
20+
21+
fun enableDebugLog(enable: Boolean) {
22+
debugLog = enable
23+
}
24+
25+
fun init(app: Application) {
26+
app.registerActivityLifecycleCallbacks(lifecycleCallback)
27+
}
28+
29+
fun getFocusedTask(): ActivityTask? {
30+
synchronized(allTasks) {
31+
if (focusedTaskId != -1) {
32+
return allTasks[focusedTaskId]?.makeCopy()
33+
}
34+
return null
35+
}
36+
}
37+
38+
/**
39+
* Return all tasks. The focused task will be the first element in returned list.
40+
*/
41+
fun getAllTasks(): List<ActivityTask> {
42+
synchronized(allTasks) {
43+
val result = ArrayList<ActivityTask>(allTasks.size)
44+
// always put the focused task at index 0
45+
val focusedTask = getFocusedTask()
46+
if (focusedTask != null) {
47+
result.add(focusedTask)
48+
}
49+
allTasks.values.forEach {
50+
if (it.taskId != focusedTaskId) {
51+
result.add(it.makeCopy())
52+
}
53+
}
54+
return result
55+
}
56+
}
57+
58+
@GuardedBy("allTasks")
59+
private fun getOrCreateTaskLocked(taskId: Int): ActivityTask {
60+
var task = allTasks[taskId]
61+
if (task == null) {
62+
task = ActivityTask(taskId)
63+
allTasks[taskId] = task
64+
}
65+
return task
66+
}
67+
68+
private fun updateLastActivityState(activity: Activity, state: ActivityInfo.State) {
69+
synchronized(allTasks) {
70+
val task = getOrCreateTaskLocked(activity.taskId)
71+
val appActivity = task.lastActivity(activity.componentName)
72+
appActivity.state = state
73+
}
74+
}
75+
76+
@VisibleForTesting
77+
internal class MyLifecycleCallback : Application.ActivityLifecycleCallbacks {
78+
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
79+
if (debugLog) Timber.tag(TAG).d("onCreate: %s", activity.componentName)
80+
synchronized(allTasks) {
81+
val appActivity = ActivityInfo(activity.componentName, activity.taskId)
82+
appActivity.state = ActivityInfo.State.Created
83+
val task = getOrCreateTaskLocked(activity.taskId)
84+
task.addActivity(appActivity)
85+
}
86+
}
87+
88+
override fun onActivityStarted(activity: Activity) {
89+
if (debugLog) Timber.tag(TAG).d("onStarted: %s", activity.componentName)
90+
updateLastActivityState(activity, ActivityInfo.State.Started)
91+
}
92+
93+
override fun onActivityResumed(activity: Activity) {
94+
if (debugLog) Timber.tag(TAG).d("onResumed: %s", activity.componentName)
95+
updateLastActivityState(activity, ActivityInfo.State.Resumed)
96+
focusedTaskId = activity.taskId
97+
resumedActivity = activity
98+
}
99+
100+
override fun onActivityPaused(activity: Activity) {
101+
if (debugLog) Timber.tag(TAG).d("onPaused: %s", activity.componentName)
102+
updateLastActivityState(activity, ActivityInfo.State.Paused)
103+
if (activity == resumedActivity) {
104+
focusedTaskId = -1
105+
resumedActivity = null
106+
}
107+
}
108+
109+
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
110+
if (debugLog) Timber.tag(TAG).d("onSaveState: %s", activity.componentName)
111+
}
112+
113+
override fun onActivityStopped(activity: Activity) {
114+
if (debugLog) Timber.tag(TAG).d("onStopped: %s", activity.componentName)
115+
updateLastActivityState(activity, ActivityInfo.State.Stopped)
116+
}
117+
118+
override fun onActivityDestroyed(activity: Activity) {
119+
if (debugLog) Timber.tag(TAG).d("onDestroyed: %s", activity.componentName)
120+
synchronized(allTasks) {
121+
val task = getOrCreateTaskLocked(activity.taskId)
122+
task.popActivity(activity.componentName)
123+
if (task.isEmpty()) {
124+
allTasks.remove(task.taskId)
125+
}
126+
}
127+
}
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package me.ycdev.android.lib.common.activity
2+
3+
import android.content.ComponentName
4+
import com.google.common.truth.Truth.assertThat
5+
import org.junit.Test
6+
import org.junit.runner.RunWith
7+
import org.robolectric.RobolectricTestRunner
8+
9+
@RunWith(RobolectricTestRunner::class)
10+
class ActivityInfoTest {
11+
private val testComponent = ComponentName("me.ycdev.test.pkg", "me.ycdev.test.clazz")
12+
13+
@Test
14+
fun makeCopy() {
15+
val origin = ActivityInfo(testComponent, 10, ActivityInfo.State.Started)
16+
val copied = origin.makeCopy()
17+
assertThat(copied.componentName).isEqualTo(testComponent)
18+
assertThat(copied.taskId).isEqualTo(10)
19+
assertThat(copied.state).isEqualTo(ActivityInfo.State.Started)
20+
}
21+
}

0 commit comments

Comments
 (0)