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.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS
20 import android.content.Context
21 import android.content.pm.PackageManager
22 import android.content.pm.PackageManager.PERMISSION_DENIED
23 import android.content.pm.PackageManager.PERMISSION_GRANTED
24 import android.health.connect.datatypes.*
25 import android.health.connect.datatypes.units.Length
26 import android.os.SystemClock
27 import android.util.Log
28 import androidx.test.uiautomator.*
29 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
30 import com.android.compatibility.common.util.UiAutomatorUtils2.*
31 import java.time.Duration
32 import java.time.Instant
33 import java.util.concurrent.TimeoutException
34 import java.util.regex.Pattern
35 
36 /** UI testing helper. */
37 object UiTestUtils {
38 
39     /** The label of the rescan button. */
40     const val RESCAN_BUTTON_LABEL = "Scan device"
41 
42     private val WAIT_TIMEOUT = Duration.ofSeconds(5)
43     private val NOT_DISPLAYED_TIMEOUT = Duration.ofMillis(500)
44 
45     private val TAG = UiTestUtils::class.java.simpleName
46 
47     private val TEST_DEVICE: Device =
48         Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build()
49 
50     private val PACKAGE_NAME = "android.healthconnect.cts.ui"
51 
52     const val TEST_APP_PACKAGE_NAME = "android.healthconnect.cts.app"
53 
54     private val TEST_APP_2_PACKAGE_NAME = "android.healthconnect.cts.app2"
55 
56     const val TEST_APP_NAME = "Health Connect cts test app"
57 
58     private const val MASK_PERMISSION_FLAGS =
59         (PackageManager.FLAG_PERMISSION_USER_SET or
60             PackageManager.FLAG_PERMISSION_USER_FIXED or
61             PackageManager.FLAG_PERMISSION_AUTO_REVOKED)
62 
63     /**
64      * Waits for the given [selector] to be displayed and performs the given [uiObjectAction] on it.
65      */
waitDisplayednull66     fun waitDisplayed(
67         selector: BySelector,
68         waitTimeout: Duration = WAIT_TIMEOUT,
69         uiObjectAction: (UiObject2) -> Unit = {}
70     ) {
<lambda>null71         waitFor("$selector to be displayed", waitTimeout) {
72             uiObjectAction(waitFindObject(selector, it.toMillis()))
73             true
74         }
75     }
76 
scrollDownTonull77     fun scrollDownTo(selector: BySelector) {
78         waitFindObject(By.scrollable(true)).scrollUntil(Direction.DOWN, Until.findObject(selector))
79     }
80 
scrollDownToAndClicknull81     fun scrollDownToAndClick(selector: BySelector) {
82         getUiDevice()
83             .findObject(By.scrollable(true))
84             .scrollUntil(Direction.DOWN, Until.findObject(selector))
85             .click()
86         getUiDevice().waitForIdle()
87     }
88 
skipOnboardingIfAppearsnull89     fun skipOnboardingIfAppears() {
90         try {
91             clickOnText("Get started")
92         } catch (e: Exception) {
93             try {
94                 clickOnText("GET STARTED")
95             } catch (e: Exception) {
96                 // No-op if onboarding was not displayed.
97             }
98         }
99     }
100 
101     /** Clicks on [UiObject2] with given [text]. */
clickOnTextnull102     fun clickOnText(string: String) {
103         waitDisplayed(By.text(string)) { it.click() }
104     }
105 
106     /** Clicks on [UiObject2] if the description contains given [string]. */
clickOnDescContainsnull107     fun clickOnDescContains(string: String) {
108         waitDisplayed(By.descContains(string)) { it.click() }
109     }
110 
deleteAllDataAndNavigateToHomeScreennull111     fun deleteAllDataAndNavigateToHomeScreen() {
112         navigateBackToHomeScreen()
113         clickOnText("Data and access")
114         clickOnText("Delete all data")
115         try {
116             clickOnText("Delete all data")
117             clickOnText("Next")
118             clickOnText("Delete")
119             clickOnText("Done")
120         } catch (e: Exception) {
121             // No-op if all data is already deleted and the delete button is disabled.
122         }
123         navigateBackToHomeScreen()
124     }
125 
navigateBackToHomeScreennull126     fun navigateBackToHomeScreen() {
127         while (isNotDisplayed("Permissions and data")) {
128             try {
129                 waitDisplayed(By.desc("Navigate up"))
130                 clickOnContentDescription("Navigate up")
131             } catch (e: Exception) {
132                 break
133             }
134         }
135     }
136 
isNotDisplayednull137     private fun isNotDisplayed(text: String): Boolean {
138         try {
139             waitNotDisplayed(By.text(text))
140             return true
141         } catch (e: Exception) {
142             return false
143         }
144     }
145 
navigateUpnull146     fun navigateUp() {
147         clickOnContentDescription("Navigate up")
148     }
149 
150     /** Clicks on [UiObject2] with given [string] content description. */
clickOnContentDescriptionnull151     fun clickOnContentDescription(string: String) {
152         waitDisplayed(By.desc(string)) { it.click() }
153     }
154 
155     /** Waits for all the given [textToFind] to be displayed. */
waitAllTextDisplayednull156     fun waitAllTextDisplayed(vararg textToFind: CharSequence?) {
157         for (text in textToFind) {
158             if (text != null) waitDisplayed(By.text(text.toString()))
159         }
160     }
161 
162     /** Waits for the given [selector] not to be displayed. */
waitNotDisplayednull163     fun waitNotDisplayed(selector: BySelector) {
164         waitFor("$selector not to be displayed", NOT_DISPLAYED_TIMEOUT) {
165             waitFindObjectOrNull(selector, it.toMillis()) == null
166         }
167     }
168 
169     /** Waits for all the given [textToFind] not to be displayed. */
waitAllTextNotDisplayednull170     fun waitAllTextNotDisplayed(vararg textToFind: CharSequence?) {
171         for (text in textToFind) {
172             if (text != null) waitNotDisplayed(By.text(text.toString()))
173         }
174     }
175 
176     /** Waits for a button with the given [label] not to be displayed. */
waitButtonNotDisplayednull177     fun waitButtonNotDisplayed(label: CharSequence) {
178         waitNotDisplayed(buttonSelector(label))
179     }
180 
rotatenull181     fun UiDevice.rotate() {
182         unfreezeRotation()
183         if (isNaturalOrientation) {
184             setOrientationLeft()
185         } else {
186             setOrientationNatural()
187         }
188         freezeRotation()
189         waitForIdle()
190     }
191 
resetRotationnull192     fun UiDevice.resetRotation() {
193         if (!isNaturalOrientation) {
194             unfreezeRotation()
195             setOrientationNatural()
196             freezeRotation()
197             waitForIdle()
198         }
199     }
200 
buttonSelectornull201     private fun buttonSelector(label: CharSequence): BySelector {
202         return By.clickable(true).text(Pattern.compile("$label|${label.toString().uppercase()}"))
203     }
204 
waitFornull205     private fun waitFor(
206         message: String,
207         uiAutomatorConditionTimeout: Duration,
208         uiAutomatorCondition: (Duration) -> Boolean,
209     ) {
210         val elapsedStartMillis = SystemClock.elapsedRealtime()
211         while (true) {
212             getUiDevice().waitForIdle()
213             val durationSinceStart =
214                 Duration.ofMillis(SystemClock.elapsedRealtime() - elapsedStartMillis)
215             if (durationSinceStart >= uiAutomatorConditionTimeout) {
216                 break
217             }
218             val remainingTime = uiAutomatorConditionTimeout - durationSinceStart
219             val uiAutomatorTimeout = minOf(uiAutomatorConditionTimeout, remainingTime)
220             try {
221                 if (uiAutomatorCondition(uiAutomatorTimeout)) {
222                     return
223                 } else {
224                     Log.d(TAG, "Failed condition for $message, will retry if within timeout")
225                 }
226             } catch (e: StaleObjectException) {
227                 Log.d(TAG, "StaleObjectException for $message, will retry if within timeout", e)
228             }
229         }
230 
231         throw TimeoutException("Timed out waiting for $message")
232     }
233 
stepsRecordFromTestAppnull234     fun stepsRecordFromTestApp(): StepsRecord {
235         return stepsRecord(TEST_APP_PACKAGE_NAME, /* stepCount= */ 10)
236     }
237 
stepsRecordFromTestAppnull238     fun stepsRecordFromTestApp(stepCount: Long): StepsRecord {
239         return stepsRecord(TEST_APP_PACKAGE_NAME, stepCount)
240     }
241 
stepsRecordFromTestAppnull242     fun stepsRecordFromTestApp(startTime: Instant): StepsRecord {
243         return stepsRecord(
244             TEST_APP_PACKAGE_NAME, /* stepCount= */ 10, startTime, startTime.plusSeconds(100))
245     }
246 
stepsRecordFromTestAppnull247     fun stepsRecordFromTestApp(stepCount: Long, startTime: Instant): StepsRecord {
248         return stepsRecord(TEST_APP_PACKAGE_NAME, stepCount, startTime, startTime.plusSeconds(100))
249     }
250 
stepsRecordFromTestApp2null251     fun stepsRecordFromTestApp2(): StepsRecord {
252         return stepsRecord(TEST_APP_2_PACKAGE_NAME, /* stepCount= */ 10)
253     }
254 
distanceRecordFromTestAppnull255     fun distanceRecordFromTestApp(): DistanceRecord {
256         return distanceRecord(TEST_APP_PACKAGE_NAME)
257     }
258 
distanceRecordFromTestAppnull259     fun distanceRecordFromTestApp(startTime: Instant): DistanceRecord {
260         return distanceRecord(TEST_APP_PACKAGE_NAME, startTime, startTime.plusSeconds(100))
261     }
262 
distanceRecordFromTestApp2null263     fun distanceRecordFromTestApp2(): DistanceRecord {
264         return distanceRecord(TEST_APP_2_PACKAGE_NAME)
265     }
266 
stepsRecordnull267     private fun stepsRecord(packageName: String, stepCount: Long): StepsRecord {
268         return stepsRecord(packageName, stepCount, Instant.now().minusMillis(1000), Instant.now())
269     }
270 
stepsRecordnull271     private fun stepsRecord(
272         packageName: String,
273         stepCount: Long,
274         startTime: Instant,
275         endTime: Instant
276     ): StepsRecord {
277         val dataOrigin: DataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
278         val testMetadataBuilder: Metadata.Builder = Metadata.Builder()
279         testMetadataBuilder.setDevice(TEST_DEVICE).setDataOrigin(dataOrigin)
280         testMetadataBuilder.setClientRecordId("SR" + Math.random())
281         return StepsRecord.Builder(testMetadataBuilder.build(), startTime, endTime, stepCount)
282             .build()
283     }
284 
distanceRecordnull285     private fun distanceRecord(
286         packageName: String,
287         startTime: Instant,
288         endTime: Instant
289     ): DistanceRecord {
290         val dataOrigin: DataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
291         val testMetadataBuilder: Metadata.Builder = Metadata.Builder()
292         testMetadataBuilder.setDevice(TEST_DEVICE).setDataOrigin(dataOrigin)
293         testMetadataBuilder.setClientRecordId("SR" + Math.random())
294         return DistanceRecord.Builder(
295                 testMetadataBuilder.build(), startTime, endTime, Length.fromMeters(500.0))
296             .build()
297     }
298 
distanceRecordnull299     private fun distanceRecord(packageName: String): DistanceRecord {
300         val dataOrigin: DataOrigin = DataOrigin.Builder().setPackageName(packageName).build()
301         val testMetadataBuilder: Metadata.Builder = Metadata.Builder()
302         testMetadataBuilder.setDevice(TEST_DEVICE).setDataOrigin(dataOrigin)
303         testMetadataBuilder.setClientRecordId("SR" + Math.random())
304         return DistanceRecord.Builder(
305                 testMetadataBuilder.build(),
306                 Instant.now().minusMillis(1000),
307                 Instant.now(),
308                 Length.fromMeters(500.0))
309             .build()
310     }
311 
grantPermissionViaPackageManagernull312     fun grantPermissionViaPackageManager(context: Context, packageName: String, permName: String) {
313         val pm = context.packageManager
314         if (pm.checkPermission(permName, packageName) == PERMISSION_GRANTED) {
315             return
316         }
317         runWithShellPermissionIdentity(
318             { pm.grantRuntimePermission(packageName, permName, context.user) },
319             Manifest.permission.GRANT_RUNTIME_PERMISSIONS)
320     }
321 
revokePermissionViaPackageManagernull322     fun revokePermissionViaPackageManager(context: Context, packageName: String, permName: String) {
323         val pm = context.packageManager
324 
325         if (pm.checkPermission(permName, packageName) == PERMISSION_DENIED) {
326             runWithShellPermissionIdentity(
327                 {
328                     pm.updatePermissionFlags(
329                         permName,
330                         packageName,
331                         MASK_PERMISSION_FLAGS,
332                         PackageManager.FLAG_PERMISSION_USER_SET,
333                         context.user)
334                 },
335                 REVOKE_RUNTIME_PERMISSIONS)
336             return
337         }
338         runWithShellPermissionIdentity(
339             { pm.revokeRuntimePermission(packageName, permName, context.user, /* reason= */ "") },
340             REVOKE_RUNTIME_PERMISSIONS)
341     }
342 
setFontnull343     fun setFont(device: UiDevice) {
344         with(device) {
345             executeShellCommand(
346                 "shell settings put system font_scale 0.85")
347         }
348     }
349 }
350