• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 android.healthconnect.cts.lib
17 
18 import android.Manifest
19 import android.content.Context
20 import android.health.connect.datatypes.*
21 import android.health.connect.datatypes.units.Length
22 import android.os.SystemClock
23 import android.util.Log
24 import androidx.test.uiautomator.By
25 import androidx.test.uiautomator.BySelector
26 import androidx.test.uiautomator.StaleObjectException
27 import androidx.test.uiautomator.UiDevice
28 import androidx.test.uiautomator.UiObject2
29 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
30 import com.android.compatibility.common.util.UiAutomatorUtils2.getUiDevice
31 import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObject
32 import com.android.compatibility.common.util.UiAutomatorUtils2.waitFindObjectOrNull
33 import java.lang.Exception
34 import java.time.Duration
35 import java.time.Instant
36 import java.util.concurrent.TimeoutException
37 import java.util.regex.Pattern
38 
39 /** UI testing helper. */
40 object UiTestUtils {
41 
42     /** The label of the rescan button. */
43     const val RESCAN_BUTTON_LABEL = "Scan device"
44 
45     private val WAIT_TIMEOUT = Duration.ofSeconds(4)
46     private val NOT_DISPLAYED_TIMEOUT = Duration.ofMillis(500)
47 
48     private val TAG = UiTestUtils::class.java.simpleName
49 
50     private val TEST_DEVICE: Device =
51         Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build()
52 
53     private val PACKAGE_NAME = "android.healthconnect.cts.ui"
54 
55     const val TEST_APP_PACKAGE_NAME = "android.healthconnect.cts.app"
56 
57     private val TEST_APP_2_PACKAGE_NAME = "android.healthconnect.cts.app2"
58 
59     const val TEST_APP_NAME = "Health Connect cts test app"
60 
61     /**
62      * Waits for the given [selector] to be displayed and performs the given [uiObjectAction] on it.
63      */
<lambda>null64     fun waitDisplayed(selector: BySelector, uiObjectAction: (UiObject2) -> Unit = {}) {
<lambda>null65         waitFor("$selector to be displayed", WAIT_TIMEOUT) {
66             uiObjectAction(waitFindObject(selector, it.toMillis()))
67             true
68         }
69     }
70 
skipOnboardingIfAppearsnull71     fun skipOnboardingIfAppears() {
72         try {
73             clickOnText("Get started")
74         } catch (e: Exception) {
75             try {
76                 clickOnText("GET STARTED")
77             } catch (e: Exception) {
78                 // No-op if onboarding was not displayed.
79             }
80         }
81     }
82 
83     /** Clicks on [UiObject2] with given [text]. */
clickOnTextnull84     fun clickOnText(string: String) {
85         waitDisplayed(By.text(string)) { it.click() }
86     }
87 
deleteAllDataAndNavigateToHomeScreennull88     fun deleteAllDataAndNavigateToHomeScreen() {
89         navigateBackToHomeScreen()
90         clickOnText("Data and access")
91         clickOnText("Delete all data")
92         try {
93             clickOnText("Delete all data")
94             clickOnText("Next")
95             clickOnText("Delete")
96             clickOnText("Done")
97         } catch (e: Exception) {
98             // No-op if all data is already deleted and the delete button is disabled.
99         }
100         navigateBackToHomeScreen()
101     }
102 
navigateBackToHomeScreennull103     fun navigateBackToHomeScreen() {
104         while (isNotDisplayed("Permissions and data")) {
105             try {
106                 waitDisplayed(By.desc("Navigate up"))
107                 clickOnContentDescription("Navigate up")
108             } catch (e: Exception) {
109                 break
110             }
111         }
112     }
113 
isNotDisplayednull114     private fun isNotDisplayed(text: String): Boolean {
115         try {
116             waitNotDisplayed(By.text(text))
117             return true
118         } catch (e: Exception) {
119             return false
120         }
121     }
122 
navigateUpnull123     fun navigateUp() {
124         clickOnContentDescription("Navigate up")
125     }
126 
127     /** Clicks on [UiObject2] with given [string] content description. */
clickOnContentDescriptionnull128     fun clickOnContentDescription(string: String) {
129         waitDisplayed(By.desc(string)) { it.click() }
130     }
131 
132     /** Waits for all the given [textToFind] to be displayed. */
waitAllTextDisplayednull133     fun waitAllTextDisplayed(vararg textToFind: CharSequence?) {
134         for (text in textToFind) {
135             if (text != null) waitDisplayed(By.text(text.toString()))
136         }
137     }
138 
139     /**
140      * Waits for a button with the given [label] to be displayed and performs the given
141      * [uiObjectAction] on it.
142      */
<lambda>null143     fun waitButtonDisplayed(label: CharSequence, uiObjectAction: (UiObject2) -> Unit = {}) =
144         waitDisplayed(buttonSelector(label), uiObjectAction)
145 
146     /** Waits for the given [selector] not to be displayed. */
waitNotDisplayednull147     fun waitNotDisplayed(selector: BySelector) {
148         waitFor("$selector not to be displayed", NOT_DISPLAYED_TIMEOUT) {
149             waitFindObjectOrNull(selector, it.toMillis()) == null
150         }
151     }
152 
153     /** Waits for all the given [textToFind] not to be displayed. */
waitAllTextNotDisplayednull154     fun waitAllTextNotDisplayed(vararg textToFind: CharSequence?) {
155         for (text in textToFind) {
156             if (text != null) waitNotDisplayed(By.text(text.toString()))
157         }
158     }
159 
160     /** Waits for a button with the given [label] not to be displayed. */
waitButtonNotDisplayednull161     fun waitButtonNotDisplayed(label: CharSequence) {
162         waitNotDisplayed(buttonSelector(label))
163     }
164 
rotatenull165     fun UiDevice.rotate() {
166         unfreezeRotation()
167         if (isNaturalOrientation) {
168             setOrientationLeft()
169         } else {
170             setOrientationNatural()
171         }
172         freezeRotation()
173         waitForIdle()
174     }
175 
resetRotationnull176     fun UiDevice.resetRotation() {
177         if (!isNaturalOrientation) {
178             unfreezeRotation()
179             setOrientationNatural()
180             freezeRotation()
181             waitForIdle()
182         }
183     }
184 
buttonSelectornull185     private fun buttonSelector(label: CharSequence): BySelector {
186         return By.clickable(true).text(Pattern.compile("$label|${label.toString().uppercase()}"))
187     }
188 
waitFornull189     private fun waitFor(
190         message: String,
191         uiAutomatorConditionTimeout: Duration,
192         uiAutomatorCondition: (Duration) -> Boolean,
193     ) {
194         val elapsedStartMillis = SystemClock.elapsedRealtime()
195         while (true) {
196             getUiDevice().waitForIdle()
197             val durationSinceStart =
198                 Duration.ofMillis(SystemClock.elapsedRealtime() - elapsedStartMillis)
199             if (durationSinceStart >= WAIT_TIMEOUT) {
200                 break
201             }
202             val remainingTime = WAIT_TIMEOUT - durationSinceStart
203             val uiAutomatorTimeout = minOf(uiAutomatorConditionTimeout, remainingTime)
204             try {
205                 if (uiAutomatorCondition(uiAutomatorTimeout)) {
206                     return
207                 } else {
208                     Log.d(TAG, "Failed condition for $message, will retry if within timeout")
209                 }
210             } catch (e: StaleObjectException) {
211                 Log.d(TAG, "StaleObjectException for $message, will retry if within timeout", e)
212             }
213         }
214 
215         throw TimeoutException("Timed out waiting for $message")
216     }
217 
stepsRecordFromTestAppnull218     fun stepsRecordFromTestApp(): StepsRecord {
219         return stepsRecord(TEST_APP_PACKAGE_NAME, /* stepCount= */ 10)
220     }
221 
stepsRecordFromTestAppnull222     fun stepsRecordFromTestApp(stepCount: Long): StepsRecord {
223         return stepsRecord(TEST_APP_PACKAGE_NAME, stepCount)
224     }
225 
stepsRecordFromTestAppnull226     fun stepsRecordFromTestApp(startTime: Instant): StepsRecord {
227         return stepsRecord(
228             TEST_APP_PACKAGE_NAME, /* stepCount= */ 10, startTime, startTime.plusSeconds(100))
229     }
230 
stepsRecordFromTestAppnull231     fun stepsRecordFromTestApp(stepCount: Long, startTime: Instant): StepsRecord {
232         return stepsRecord(TEST_APP_PACKAGE_NAME, stepCount, startTime, startTime.plusSeconds(100))
233     }
234 
stepsRecordFromTestApp2null235     fun stepsRecordFromTestApp2(): StepsRecord {
236         return stepsRecord(TEST_APP_2_PACKAGE_NAME, /* stepCount= */ 10)
237     }
238 
distanceRecordFromTestAppnull239     fun distanceRecordFromTestApp(): DistanceRecord {
240         return distanceRecord(TEST_APP_PACKAGE_NAME)
241     }
242 
distanceRecordFromTestApp2null243     fun distanceRecordFromTestApp2(): DistanceRecord {
244         return distanceRecord(TEST_APP_2_PACKAGE_NAME)
245     }
246 
stepsRecordnull247     private fun stepsRecord(packageName: String, stepCount: Long): StepsRecord {
248         return stepsRecord(packageName, stepCount, Instant.now().minusMillis(1000), Instant.now())
249     }
250 
stepsRecordnull251     private fun stepsRecord(
252         packageName: String,
253         stepCount: Long,
254         startTime: Instant,
255         endTime: Instant
256     ): StepsRecord {
257         val dataOrigin: DataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
258         val testMetadataBuilder: Metadata.Builder = Metadata.Builder()
259         testMetadataBuilder.setDevice(TEST_DEVICE).setDataOrigin(dataOrigin)
260         testMetadataBuilder.setClientRecordId("SR" + Math.random())
261         return StepsRecord.Builder(testMetadataBuilder.build(), startTime, endTime, stepCount)
262             .build()
263     }
264 
distanceRecordnull265     private fun distanceRecord(packageName: String): DistanceRecord {
266         val dataOrigin: DataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
267         val testMetadataBuilder: Metadata.Builder = Metadata.Builder()
268         testMetadataBuilder.setDevice(TEST_DEVICE).setDataOrigin(dataOrigin)
269         testMetadataBuilder.setClientRecordId("SR" + Math.random())
270         return DistanceRecord.Builder(
271                 testMetadataBuilder.build(),
272                 Instant.now().minusMillis(1000),
273                 Instant.now(),
274                 Length.fromMeters(500.0))
275             .build()
276     }
277 
grantPermissionViaPackageManagernull278     fun grantPermissionViaPackageManager(context: Context, packageName: String, permName: String) {
279         runWithShellPermissionIdentity(
280             { context.packageManager.grantRuntimePermission(packageName, permName, context.user) },
281             Manifest.permission.GRANT_RUNTIME_PERMISSIONS)
282     }
283 
revokePermissionViaPackageManagernull284     fun revokePermissionViaPackageManager(context: Context, packageName: String, permName: String) {
285         runWithShellPermissionIdentity(
286             {
287                 context.packageManager.revokeRuntimePermission(
288                     packageName, permName, context.user, /* reason= */ "")
289             },
290             Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
291     }
292 }
293