1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.util
17 
18 import android.content.Context
19 import android.content.Intent
20 import android.os.SystemClock
21 import android.view.InputDevice
22 import android.view.KeyCharacterMap
23 import android.view.KeyEvent
24 import android.view.MotionEvent
25 import android.view.View
26 import android.view.ViewGroup
27 import androidx.core.view.children
28 import androidx.lifecycle.Lifecycle.State.RESUMED
29 import androidx.test.core.app.ActivityScenario
30 import androidx.test.core.app.ActivityScenario.ActivityAction
31 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
32 import androidx.test.uiautomator.UiDevice
33 import com.android.launcher3.Launcher
34 import com.android.launcher3.LauncherAppState
35 import com.android.launcher3.LauncherState
36 import com.android.launcher3.R
37 import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
38 import com.android.launcher3.tapl.TestHelpers
39 import com.android.launcher3.util.ModelTestExtensions.loadModelSync
40 import com.android.launcher3.util.Wait.atMost
41 import java.util.function.Function
42 import java.util.function.Predicate
43 import java.util.function.Supplier
44 import org.junit.After
45 
46 /**
47  * Base class for tests which use Launcher activity with some utility methods.
48  *
49  * This should instead be a rule, but is kept as a base class for easier migration from TAPL
50  */
51 open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
52 
53     private var currentScenario: ActivityScenario<LAUNCHER_TYPE>? = null
54 
55     val scenario: ActivityScenario<LAUNCHER_TYPE>
56         get() =
57             currentScenario
58                 ?: ActivityScenario.launch<LAUNCHER_TYPE>(
59                         TestHelpers.getHomeIntentInPackage(targetContext()),
60                         null,
61                     )
<lambda>null62                     .also { currentScenario = it }
63 
64     @JvmField val uiDevice = UiDevice.getInstance(getInstrumentation())
65 
66     @After
closeCurrentActivitynull67     fun closeCurrentActivity() {
68         currentScenario?.close()
69         currentScenario = null
70     }
71 
loadLauncherSyncnull72     protected fun loadLauncherSync() {
73         LauncherAppState.getInstance(targetContext()).model.loadModelSync()
74         scenario.moveToState(RESUMED)
75     }
76 
targetContextnull77     protected fun targetContext(): Context = getInstrumentation().targetContext
78 
79     protected fun goToState(state: LauncherState) {
80         executeOnLauncher { it.stateManager.goToState(state, 0) }
81         UiDevice.getInstance(getInstrumentation()).waitForIdle()
82     }
83 
executeOnLaunchernull84     protected fun executeOnLauncher(f: ActivityAction<LAUNCHER_TYPE>) = scenario.onActivity(f)
85 
86     protected fun <T> getFromLauncher(f: Function<in LAUNCHER_TYPE, out T?>): T? {
87         var result: T? = null
88         executeOnLauncher { result = f.apply(it) }
89         return result
90     }
91 
isInStatenull92     protected fun isInState(state: Supplier<LauncherState>): Boolean =
93         getFromLauncher { it.stateManager.state == state.get() }!!
94 
waitForStatenull95     protected fun waitForState(message: String, state: Supplier<LauncherState>) =
96         waitForLauncherCondition(message) { it.stateManager.currentStableState === state.get() }
97 
waitForLauncherConditionnull98     protected fun waitForLauncherCondition(
99         message: String,
100         condition: Function<LAUNCHER_TYPE, Boolean>,
101     ) = atMost(message, { getFromLauncher(condition)!! })
102 
waitForLauncherConditionnull103     protected fun waitForLauncherCondition(
104         message: String,
105         condition: Function<LAUNCHER_TYPE, Boolean>,
106         timeout: Long,
107     ) = atMost(message, { getFromLauncher(condition)!! }, null, timeout)
108 
getOnceNotNullnull109     protected fun <T> getOnceNotNull(message: String, f: Function<LAUNCHER_TYPE, T?>): T? {
110         var output: T? = null
111         atMost(
112             message,
113             {
114                 val fromLauncher = getFromLauncher<T>(f)
115                 output = fromLauncher
116                 fromLauncher != null
117             },
118         )
119         return output
120     }
121 
getAllAppsScrollnull122     protected fun getAllAppsScroll(launcher: LAUNCHER_TYPE) =
123         launcher.appsView.activeRecyclerView.computeVerticalScrollOffset()
124 
125     @JvmOverloads
126     protected fun injectKeyEvent(keyCode: Int, actionDown: Boolean, metaState: Int = 0) {
127         uiDevice.waitForIdle()
128         val eventTime = SystemClock.uptimeMillis()
129         val event =
130             KeyEvent(
131                 eventTime,
132                 eventTime,
133                 if (actionDown) KeyEvent.ACTION_DOWN else MotionEvent.ACTION_UP,
134                 keyCode,
135                 /* repeat= */ 0,
136                 metaState,
137                 KeyCharacterMap.VIRTUAL_KEYBOARD,
138                 /* scancode= */ 0,
139                 /* flags= */ 0,
140                 InputDevice.SOURCE_KEYBOARD,
141             )
142         executeOnLauncher { it.dispatchKeyEvent(event) }
143     }
144 
145     @JvmOverloads
startAppFastnull146     fun startAppFast(
147         packageName: String,
148         intent: Intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!,
149     ) {
150         intent.addCategory(Intent.CATEGORY_LAUNCHER)
151         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
152         targetContext().startActivity(intent)
153         uiDevice.waitForIdle()
154     }
155 
<lambda>null156     fun freezeAllApps() = executeOnLauncher {
157         it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
158     }
159 
executeShellCommandnull160     fun executeShellCommand(cmd: String) = uiDevice.executeShellCommand(cmd)
161 
162     fun addToWorkspace(view: View) {
163         TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
164             view.accessibilityDelegate.performAccessibilityAction(
165                 view,
166                 R.id.action_add_to_workspace,
167                 null,
168             )
169         }
170         UiDevice.getInstance(getInstrumentation()).waitForIdle()
171     }
172 
173     /**
174      * Match the behavior with how widget is added in reality with "tap to add" (even with screen
175      * readers).
176      */
addWidgetToWorkspacenull177     fun addWidgetToWorkspace(view: View) {
178         executeOnLauncher {
179             view.performClick()
180             UiDevice.getInstance(getInstrumentation()).waitForIdle()
181             view.findViewById<View>(R.id.widget_add_button).performClick()
182         }
183     }
184 
searchViewnull185     fun ViewGroup.searchView(filter: Predicate<View>): View? {
186         if (filter.test(this)) return this
187         for (child in children) {
188             if (filter.test(child)) return child
189             if (child is ViewGroup)
190                 child.searchView(filter)?.let {
191                     return it
192                 }
193         }
194         return null
195     }
196 }
197