1 /*
2  * Copyright (C) 2021 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 
17 package android.companion.cts.uicommon
18 
19 import android.os.SystemClock
20 import android.os.SystemClock.sleep
21 import androidx.test.uiautomator.By
22 import androidx.test.uiautomator.BySelector
23 import androidx.test.uiautomator.Direction
24 import androidx.test.uiautomator.SearchCondition
25 import androidx.test.uiautomator.UiDevice
26 import androidx.test.uiautomator.UiObject2
27 import androidx.test.uiautomator.Until
28 import kotlin.time.Duration
29 import kotlin.time.Duration.Companion.seconds
30 
31 open class CompanionDeviceManagerUi(private val ui: UiDevice) {
32     val isVisible: Boolean
33         get() = ui.hasObject(CONFIRMATION_UI)
34 
dismissnull35     fun dismiss() {
36         if (!isVisible) return
37         // Pressing back button should close (cancel) confirmation UI.
38         ui.pressBack()
39         waitUntilGone()
40     }
41 
waitUntilVisiblenull42     fun waitUntilVisible(timeout: Duration = 3.seconds) = ui.wait(
43         Until.hasObject(CONFIRMATION_UI), "CDM UI has not appeared.", timeout)
44 
45     fun waitUntilNotificationVisible(isAuto: Boolean = false) = ui.wait(
46         if (isAuto) Until.hasObject(NOTIFICATION_UI_AUTO) else Until.hasObject(NOTIFICATION_UI),
47         "NOTIFICATION UI has not appeared.")
48 
49     fun waitUntilGone() = ui.waitShort(Until.gone(CONFIRMATION_UI), "CDM UI has not disappeared")
50 
51     fun waitAndClickOnFirstFoundDevice() {
52         val firstDevice = ui.waitLongAndFind(
53                 Until.findObject(
54                         DEVICE_LIST_WITH_ITEMS), "The item in the Device List not found or empty")
55                 .children[0]
56 
57         val startTime = SystemClock.uptimeMillis()
58         var elapsedTime = 0L
59         // Keep trying to click the first item in the list until the device_list is disappeared
60         // or it times out after 5s.
61         while (ui.hasObject(DEVICE_LIST) && elapsedTime < 5.seconds.inWholeMilliseconds) {
62             firstDevice.click()
63             sleep(0.2.seconds.inWholeMilliseconds)
64             elapsedTime = SystemClock.uptimeMillis() - startTime
65         }
66     }
67 
waitUntilPositiveButtonIsEnabledAndClicknull68     fun waitUntilPositiveButtonIsEnabledAndClick() = ui.waitLongAndFind(
69         Until.findObject(POSITIVE_BUTTON), "Positive button not found or not clickable")
70             .click()
71 
72     fun waitUntilSystemDataTransferConfirmationVisible() = ui.wait(
73             Until.hasObject(SYSTEM_DATA_TRANSFER_CONFIRMATION_UI),
74             "System data transfer dialog has not appeared.")
75 
76     fun clickPositiveButton() = click(POSITIVE_BUTTON, "Positive button")
77 
78     fun clickNegativeButton() = click(NEGATIVE_BUTTON, "Negative button")
79 
80     fun clickNegativeButtonMultipleDevices() {
81         ui.wait(Until.findObject(CONFIRMATION_UI), 2.seconds.inWholeMilliseconds)?.let {
82             // swipe up (or scroll down) until cancel button is enabled
83             val startTime = SystemClock.uptimeMillis()
84             var elapsedTime = 0L
85             // UiDevice.hasObject() takes a long time for some reason so wait at least 10 seconds
86             while (!ui.hasObject(NEGATIVE_BUTTON_MULTIPLE_DEVICES)
87                     && elapsedTime < 10.seconds.inWholeMilliseconds) {
88                 it.swipe(Direction.UP, 1.0F)
89                 elapsedTime = SystemClock.uptimeMillis() - startTime
90             }
91         }
92         click(NEGATIVE_BUTTON_MULTIPLE_DEVICES, "Negative button for multiple devices")
93     }
94 
waitUntilAppAppearednull95     fun waitUntilAppAppeared() = ui.wait(Until.hasObject(ASSOCIATION_REVOKE_APP_UI),
96         "The test app has not appeared.")
97 
98     fun waitUntilPositiveButtonAppeared() = ui.waitLongAndFind(
99         Until.findObject(POSITIVE_BUTTON), "Positive button")
100 
101     fun scrollToBottom() {
102         ui.wait(Until.findObject(SCROLLABLE_PERMISSION_LIST), 2.seconds.inWholeMilliseconds)?.let {
103             val positiveButton = waitUntilPositiveButtonAppeared()
104 
105             // swipe up (or scroll down) until "Allow" button is enabled
106             val startTime = SystemClock.uptimeMillis()
107             var elapsedTime = 0L
108             while (!positiveButton.isEnabled && elapsedTime < 5.seconds.inWholeMilliseconds) {
109                 it.swipe(Direction.UP, 1.0F)
110 
111                 // Wait before consecutive swipes
112                 if (!positiveButton.isEnabled) {
113                     sleep(0.2.seconds.inWholeMilliseconds)
114                 }
115                 elapsedTime = SystemClock.uptimeMillis() - startTime
116             }
117         }
118     }
119 
clicknull120     protected fun click(selector: BySelector, description: String) = ui.waitShortAndFind(
121             Until.findObject(selector), "$description is not found")
122             .click()
123 
124     companion object {
125         private const val PACKAGE_NAME = "com.android.companiondevicemanager"
126         private const val NOTIFICATION_PACKAGE_NAME = "com.android.settings"
127         private const val NOTIFICATION_PACKAGE_NAME_AUTO = "com.android.car.settings"
128 
129         private val CONFIRMATION_UI = By.pkg(PACKAGE_NAME)
130                 .res(PACKAGE_NAME, "activity_confirmation")
131         private val ASSOCIATION_REVOKE_APP_UI = By.pkg(ASSOCIATION_REVOKE_APP_NAME).depth(0)
132 
133         private val NOTIFICATION_UI = By.pkg(NOTIFICATION_PACKAGE_NAME).depth(0)
134 
135         private val NOTIFICATION_UI_AUTO = By.pkg(NOTIFICATION_PACKAGE_NAME_AUTO).depth(0)
136 
137         private val CLICKABLE_BUTTON =
138                 By.pkg(PACKAGE_NAME).clazz(".Button").clickable(true)
139         private val POSITIVE_BUTTON = By.copy(CLICKABLE_BUTTON).res(PACKAGE_NAME, "btn_positive")
140         private val NEGATIVE_BUTTON = By.copy(CLICKABLE_BUTTON).res(PACKAGE_NAME, "btn_negative")
141         private val NEGATIVE_BUTTON_MULTIPLE_DEVICES = By.pkg(PACKAGE_NAME)
142                 .res(PACKAGE_NAME, "negative_multiple_devices_layout")
143 
144         private val DEVICE_LIST = By.res(PACKAGE_NAME, "device_list")
145         private val DEVICE_LIST_ITEM = By.res(PACKAGE_NAME, "list_item_device")
146         private val DEVICE_LIST_WITH_ITEMS = By.copy(DEVICE_LIST)
147                 .hasDescendant(DEVICE_LIST_ITEM)
148 
149         private val SCROLLABLE_PERMISSION_LIST = By.pkg(PACKAGE_NAME)
150                 .res(PACKAGE_NAME, "permission_list")
151 
152         private val SYSTEM_DATA_TRANSFER_CONFIRMATION_UI = By.pkg(PACKAGE_NAME)
153                 .res(PACKAGE_NAME, "data_transfer_confirmation")
154     }
155 
waitnull156     protected fun UiDevice.wait(
157         condition: SearchCondition<Boolean>,
158         message: String,
159         timeout: Duration = 3.seconds
160     ) {
161         if (!wait(condition, timeout.inWholeMilliseconds)) error(message)
162     }
163 
waitShortnull164     protected fun UiDevice.waitShort(condition: SearchCondition<Boolean>, message: String) =
165             wait(condition, message, 1.seconds)
166 
167     protected fun UiDevice.waitAndFind(
168         condition: SearchCondition<UiObject2>,
169         message: String,
170         timeout: Duration = 3.seconds
171     ): UiObject2 =
172             wait(condition, timeout.inWholeMilliseconds) ?: error(message)
173 
174     protected fun UiDevice.waitShortAndFind(
175         condition: SearchCondition<UiObject2>,
176         message: String
177     ): UiObject2 = waitAndFind(condition, message, 1.seconds)
178 
179     protected fun UiDevice.waitLongAndFind(
180         condition: SearchCondition<UiObject2>,
181         message: String
182     ): UiObject2 = waitAndFind(condition, message, 10.seconds)
183 }
184