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