1 /*
2  * Copyright (C) 2023 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.adservices.tests.ui.libs;
17 
18 import static android.Manifest.permission.POST_NOTIFICATIONS;
19 
20 import static com.android.adservices.AdServicesCommon.BINDER_TIMEOUT_SYSTEM_PROPERTY_NAME;
21 import static com.android.adservices.service.DebugFlagsConstants.KEY_CONSENT_MANAGER_DEBUG_MODE;
22 import static com.android.adservices.service.DebugFlagsConstants.KEY_CONSENT_MANAGER_OTA_DEBUG_MODE;
23 import static com.android.adservices.service.DebugFlagsConstants.KEY_CONSENT_NOTIFICATION_DEBUG_MODE;
24 import static com.android.adservices.service.FlagsConstants.KEY_CONSENT_NOTIFICATION_RESET_TOKEN;
25 import static com.android.adservices.service.FlagsConstants.KEY_ENABLE_AD_SERVICES_SYSTEM_API;
26 import static com.android.adservices.service.FlagsConstants.KEY_GA_UX_FEATURE_ENABLED;
27 import static com.android.adservices.service.FlagsConstants.KEY_GET_ADSERVICES_COMMON_STATES_ALLOW_LIST;
28 import static com.android.adservices.service.FlagsConstants.KEY_IS_EEA_DEVICE;
29 import static com.android.adservices.service.FlagsConstants.KEY_IS_GET_ADSERVICES_COMMON_STATES_API_ENABLED;
30 import static com.android.adservices.service.FlagsConstants.KEY_PAS_UX_ENABLED;
31 import static com.android.adservices.service.FlagsConstants.KEY_U18_UX_ENABLED;
32 import static com.android.adservices.service.FlagsConstants.KEY_UI_OTA_STRINGS_FEATURE_ENABLED;
33 import static com.android.adservices.tests.ui.libs.UiConstants.SYSTEM_UI_NAME;
34 import static com.android.adservices.tests.ui.libs.UiConstants.SYSTEM_UI_RESOURCE_ID;
35 
36 import static com.google.common.truth.Truth.assertThat;
37 
38 import android.adservices.common.AdServicesCommonManager;
39 import android.adservices.common.AdServicesStates;
40 import android.content.Context;
41 import android.graphics.Point;
42 import android.os.OutcomeReceiver;
43 
44 import androidx.test.platform.app.InstrumentationRegistry;
45 import androidx.test.uiautomator.By;
46 import androidx.test.uiautomator.BySelector;
47 import androidx.test.uiautomator.Direction;
48 import androidx.test.uiautomator.SearchCondition;
49 import androidx.test.uiautomator.UiDevice;
50 import androidx.test.uiautomator.UiObject2;
51 import androidx.test.uiautomator.Until;
52 
53 import com.android.adservices.LogUtil;
54 import com.android.adservices.api.R;
55 import com.android.adservices.common.AdServicesFlagsSetterRule;
56 import com.android.adservices.shared.testing.common.FileHelper;
57 import com.android.compatibility.common.util.ShellUtils;
58 
59 import com.google.common.util.concurrent.SettableFuture;
60 
61 import java.io.File;
62 import java.text.SimpleDateFormat;
63 import java.time.Instant;
64 import java.util.Date;
65 import java.util.List;
66 import java.util.Locale;
67 import java.util.UUID;
68 import java.util.concurrent.Executors;
69 import java.util.regex.Pattern;
70 
71 public class UiUtils {
72     public static final int LAUNCH_TIMEOUT = 5000;
73     public static final int PRIMITIVE_UI_OBJECTS_LAUNCH_TIMEOUT = 500;
74     public static final int SCROLL_WAIT_TIME = 1000;
75 
76     private static final int DEFAULT_BINDER_CONNECTION_TIMEOUT_MS = 10000;
77 
78     private static final String ANDROID_WIDGET_SCROLLVIEW = "android.widget.ScrollView";
79 
80     /** Refreshes the consent reset token to a new randomly-generated value */
refreshConsentResetToken(AdServicesFlagsSetterRule flags)81     public static void refreshConsentResetToken(AdServicesFlagsSetterRule flags) {
82         flags.setFlag(KEY_CONSENT_NOTIFICATION_RESET_TOKEN, UUID.randomUUID().toString());
83     }
84 
85     /** Enables consent manager debug mode */
enableConsentDebugMode(AdServicesFlagsSetterRule flags)86     public static void enableConsentDebugMode(AdServicesFlagsSetterRule flags) {
87         flags.setDebugFlag(KEY_CONSENT_NOTIFICATION_DEBUG_MODE, true);
88     }
89 
90     /** Disables consent manager debug mode */
disableConsentDebugMode(AdServicesFlagsSetterRule flags)91     public static void disableConsentDebugMode(AdServicesFlagsSetterRule flags) {
92         flags.setDebugFlag(KEY_CONSENT_NOTIFICATION_DEBUG_MODE, false);
93     }
94 
95     /** Sets the flag to mimic the behavior of an EEA device */
setAsEuDevice(AdServicesFlagsSetterRule flags)96     public static void setAsEuDevice(AdServicesFlagsSetterRule flags) {
97         flags.setFlag(KEY_IS_EEA_DEVICE, true);
98     }
99 
100     /** Sets the flag to mimic the behavior of a non-EEA device */
setAsRowDevice(AdServicesFlagsSetterRule flags)101     public static void setAsRowDevice(AdServicesFlagsSetterRule flags) {
102         flags.setFlag(KEY_IS_EEA_DEVICE, false);
103     }
104 
105     /** Enables Under-18 UX */
enableU18(AdServicesFlagsSetterRule flags)106     public static void enableU18(AdServicesFlagsSetterRule flags) {
107         flags.setFlag(KEY_U18_UX_ENABLED, true);
108     }
109 
110     /** Sets the flag to enable GA UX */
enableGa(AdServicesFlagsSetterRule flags)111     public static void enableGa(AdServicesFlagsSetterRule flags) throws Exception {
112         flags.setFlag(KEY_GA_UX_FEATURE_ENABLED, true);
113     }
114 
115     /** Enables the enableAdServices system API. */
turnOnAdServicesSystemApi(AdServicesFlagsSetterRule flags)116     public static void turnOnAdServicesSystemApi(AdServicesFlagsSetterRule flags) {
117         flags.setFlag(KEY_ENABLE_AD_SERVICES_SYSTEM_API, true);
118     }
119 
120     /** Sets the flag to enable Beta UX */
enableBeta(AdServicesFlagsSetterRule flags)121     public static void enableBeta(AdServicesFlagsSetterRule flags) {
122         flags.setFlag(KEY_GA_UX_FEATURE_ENABLED, false);
123     }
124 
125     /** Sets the flag to disable OTA String download feature */
disableOtaStrings(AdServicesFlagsSetterRule flags)126     public static void disableOtaStrings(AdServicesFlagsSetterRule flags) throws Exception {
127         flags.setFlag(KEY_UI_OTA_STRINGS_FEATURE_ENABLED, false);
128     }
129 
130     /** Restarts the AdServices process */
restartAdservices()131     public static void restartAdservices() {
132         ShellUtils.runShellCommand("am force-stop com.google.android.adservices.api");
133         ShellUtils.runShellCommand("am force-stop com.android.adservices.api");
134     }
135 
136     /** Sets flag consent_manager_debug_mode to true in tests */
setConsentManagerDebugMode(AdServicesFlagsSetterRule flags)137     public static void setConsentManagerDebugMode(AdServicesFlagsSetterRule flags) {
138         flags.setDebugFlag(KEY_CONSENT_MANAGER_DEBUG_MODE, true);
139     }
140 
141     /** Sets flag consent_manager_ota_debug_mode to true in tests */
setConsentManagerOtaDebugMode(AdServicesFlagsSetterRule flags)142     public static void setConsentManagerOtaDebugMode(AdServicesFlagsSetterRule flags) {
143         flags.setDebugFlag(KEY_CONSENT_MANAGER_OTA_DEBUG_MODE, true);
144     }
145 
146     /** Sets flag consent_manager_debug_mode to false in tests */
resetConsentManagerDebugMode(AdServicesFlagsSetterRule flags)147     public static void resetConsentManagerDebugMode(AdServicesFlagsSetterRule flags) {
148         flags.setDebugFlag(KEY_CONSENT_MANAGER_DEBUG_MODE, false);
149     }
150 
enableNotificationPermission()151     public static void enableNotificationPermission() {
152         InstrumentationRegistry.getInstrumentation()
153                 .getUiAutomation()
154                 .grantRuntimePermission("com.android.adservices.api", POST_NOTIFICATIONS);
155     }
156 
157     /** Sets the flag to disable the V2 notification flow */
disableNotificationFlowV2(AdServicesFlagsSetterRule flags)158     public static void disableNotificationFlowV2(AdServicesFlagsSetterRule flags) throws Exception {
159         flags.setFlag("eu_notif_flow_change_enabled", false);
160     }
161 
162     /** Sets the binder time for cts test */
setBinderTimeout(AdServicesFlagsSetterRule flags)163     public static void setBinderTimeout(AdServicesFlagsSetterRule flags) {
164         flags.setDebugFlag(
165                 BINDER_TIMEOUT_SYSTEM_PROPERTY_NAME, DEFAULT_BINDER_CONNECTION_TIMEOUT_MS);
166     }
167 
168     /** Enables pas */
enablePas(AdServicesFlagsSetterRule flags)169     public static void enablePas(AdServicesFlagsSetterRule flags) {
170         flags.setFlag(KEY_PAS_UX_ENABLED, true);
171     }
172 
173     /** Disables pas */
disablePas(AdServicesFlagsSetterRule flags)174     public static void disablePas(AdServicesFlagsSetterRule flags) {
175         flags.setFlag(KEY_PAS_UX_ENABLED, false);
176     }
177 
verifyNotification( Context context, UiDevice device, boolean isDisplayed, boolean isEuTest, UiConstants.UX ux)178     public static void verifyNotification(
179             Context context,
180             UiDevice device,
181             boolean isDisplayed,
182             boolean isEuTest,
183             UiConstants.UX ux)
184             throws Exception {
185         int notificationTitle = -1;
186         int notificationHeader = -1;
187         switch (ux) {
188             case GA_UX:
189                 // Should match the contentTitle string in ConsentNotificationTrigger.java.
190                 notificationTitle =
191                         isEuTest
192                                 ? R.string.notificationUI_notification_ga_title_eu_v2
193                                 : R.string.notificationUI_notification_ga_title_v2;
194                 // Should match the text in consent_notification_screen_1_ga_v2_eu.xml and
195                 // consent_notification_screen_1_ga_v2_row.xml, respectively.
196                 notificationHeader =
197                         isEuTest
198                                 ? R.string.notificationUI_fledge_measurement_title_v2
199                                 : R.string.notificationUI_header_ga_title_v2;
200                 break;
201             case BETA_UX:
202                 notificationTitle =
203                         isEuTest
204                                 ? R.string.notificationUI_notification_title_eu
205                                 : R.string.notificationUI_notification_title;
206                 notificationHeader =
207                         isEuTest
208                                 ? R.string.notificationUI_header_title_eu
209                                 : R.string.notificationUI_header_title;
210                 break;
211             case U18_UX:
212                 notificationTitle = R.string.notificationUI_u18_notification_title;
213                 notificationHeader = R.string.notificationUI_u18_header_title;
214                 break;
215         }
216 
217         verifyNotification(context, device, isDisplayed, notificationTitle, notificationHeader);
218     }
219 
verifyNotification( Context context, UiDevice device, boolean isDisplayed, int notificationTitle, int notificationHeader)220     public static void verifyNotification(
221             Context context,
222             UiDevice device,
223             boolean isDisplayed,
224             int notificationTitle,
225             int notificationHeader)
226             throws Exception {
227         device.openNotification();
228         device.waitForIdle(LAUNCH_TIMEOUT);
229         // Wait few seconds for Adservices notification to show, waitForIdle is not enough.
230         Thread.sleep(LAUNCH_TIMEOUT);
231         UiObject2 scroller = device.findObject(By.res(SYSTEM_UI_RESOURCE_ID));
232 
233         BySelector notificationTitleSelector = By.text(getString(context, notificationTitle));
234         if (!isDisplayed) {
235             assertThat(scroller.hasObject(notificationTitleSelector)).isFalse();
236             return;
237         }
238         assertThat(scroller.hasObject(notificationTitleSelector)).isTrue();
239         UiObject2 notificationCard =
240                 scroller.findObject(By.text(getString(context, notificationTitle)));
241 
242         notificationCard.click();
243         device.waitForIdle(LAUNCH_TIMEOUT);
244         Thread.sleep(LAUNCH_TIMEOUT);
245         UiObject2 title = getElement(context, device, notificationHeader);
246         assertThat(title).isNotNull();
247     }
248 
249     /**
250      * Swipes through the screen to show elements on the button of the page but hidden by the
251      * navigation bar.
252      *
253      * @param device the UiDevice to manipulate
254      */
gentleSwipe(UiDevice device)255     public static void gentleSwipe(UiDevice device) {
256         UiObject2 scrollView =
257                 device.findObject(By.scrollable(true).clazz(ANDROID_WIDGET_SCROLLVIEW));
258         if (scrollView != null) {
259                 scrollView.scroll(Direction.DOWN, /* percent */ 0.25F);
260         }
261     }
262 
263     /** Sets the flag to enable or disable the flip flow */
setFlipFlow(AdServicesFlagsSetterRule flags, boolean isFlip)264     public static void setFlipFlow(AdServicesFlagsSetterRule flags, boolean isFlip) {
265         flags.setFlag("eu_notif_flow_change_enabled", isFlip);
266     }
267 
268     /** Sets get adservices common states services enabled */
setGetAdservicesCommonStatesServiceEnable( AdServicesFlagsSetterRule flags, boolean enable)269     public static void setGetAdservicesCommonStatesServiceEnable(
270             AdServicesFlagsSetterRule flags, boolean enable) {
271         flags.setFlag(KEY_IS_GET_ADSERVICES_COMMON_STATES_API_ENABLED, enable);
272     }
273 
274     /** Sets get adservices common states services enabled */
setGetAdservicesCommonStatesAllowList( AdServicesFlagsSetterRule flags, String list)275     public static void setGetAdservicesCommonStatesAllowList(
276             AdServicesFlagsSetterRule flags, String list) {
277         flags.setFlag(KEY_GET_ADSERVICES_COMMON_STATES_ALLOW_LIST, list);
278     }
279 
getString(Context context, int resourceId)280     public static String getString(Context context, int resourceId) {
281         return context.getResources().getString(resourceId);
282     }
283 
scrollToAndClick(Context context, UiDevice device, int resId)284     public static void scrollToAndClick(Context context, UiDevice device, int resId) {
285         scrollTo(context, device, resId);
286         UiObject2 consentPageButton =
287                 device.wait(
288                         getSearchCondByResId(context, resId), PRIMITIVE_UI_OBJECTS_LAUNCH_TIMEOUT);
289         clickTopLeft(consentPageButton);
290     }
291 
getSearchCondByResId(Context context, int resId)292     public static SearchCondition<UiObject2> getSearchCondByResId(Context context, int resId) {
293         String targetStr = getString(context, resId);
294         return Until.findObject(By.text(Pattern.compile(targetStr, Pattern.CASE_INSENSITIVE)));
295     }
296 
getPageElement(Context context, UiDevice device, int resId)297     public static UiObject2 getPageElement(Context context, UiDevice device, int resId) {
298         return device.findObject(By.text(getString(context, resId)));
299     }
300 
scrollTo(Context context, UiDevice device, int resId)301     public static UiObject2 scrollTo(Context context, UiDevice device, int resId) {
302         UiObject2 scrollView =
303                 device.findObject(By.scrollable(true).clazz(ANDROID_WIDGET_SCROLLVIEW));
304         if (scrollView != null) {
305             String targetStr = getString(context, resId);
306             scrollView.scrollUntil(
307                     Direction.DOWN,
308                     Until.findObject(
309                             By.text(Pattern.compile(targetStr, Pattern.CASE_INSENSITIVE))));
310         }
311         return getElement(context, device, resId);
312     }
313 
getElement(Context context, UiDevice device, int resId)314     public static UiObject2 getElement(Context context, UiDevice device, int resId) {
315         return getElement(context, device, resId, 0);
316     }
317 
getElement(Context context, UiDevice device, int resId, int index)318     public static UiObject2 getElement(Context context, UiDevice device, int resId, int index) {
319         String targetStr = getString(context, resId);
320         List<UiObject2> objList =
321                 device.findObjects(By.text(Pattern.compile(targetStr, Pattern.CASE_INSENSITIVE)));
322         if (objList.size() <= index) {
323             return null;
324         }
325         return objList.get(index);
326     }
327 
clickTopLeft(UiObject2 obj)328     public static void clickTopLeft(UiObject2 obj) {
329         assertThat(obj).isNotNull();
330         obj.click(new Point(obj.getVisibleBounds().top, obj.getVisibleBounds().left));
331     }
332 
takeScreenshot(UiDevice device, String methodName)333     public static void takeScreenshot(UiDevice device, String methodName) {
334         try {
335             String timeStamp =
336                     new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
337                             .format(Date.from(Instant.now()));
338 
339             File screenshotFile =
340                     new File(
341                             FileHelper.getAdServicesTestsOutputDir(),
342                             methodName + timeStamp + ".png");
343             device.takeScreenshot(screenshotFile);
344         } catch (RuntimeException e) {
345             LogUtil.e("Failed to take screenshot: " + e.getMessage());
346         }
347     }
348 
349     /** Resets AdServices consent data */
resetAdServicesConsentData(Context context, AdServicesFlagsSetterRule flags)350     public static void resetAdServicesConsentData(Context context, AdServicesFlagsSetterRule flags)
351             throws Exception {
352         // Neeed to disable debug mode since it takes precedence over reset channel.
353         disableConsentDebugMode(flags);
354         turnOnAdServicesSystemApi(flags);
355 
356         AdServicesCommonManager mCommonManager = AdServicesCommonManager.get(context);
357 
358         // Reset consent and thereby AdServices data before each test.
359         UiUtils.refreshConsentResetToken(flags);
360 
361         SettableFuture<Boolean> responseFuture = SettableFuture.create();
362 
363         mCommonManager.enableAdServices(
364                 new AdServicesStates.Builder()
365                         .setAdIdEnabled(true)
366                         .setAdultAccount(true)
367                         .setU18Account(true)
368                         .setPrivacySandboxUiEnabled(true)
369                         .setPrivacySandboxUiRequest(false)
370                         .build(),
371                 Executors.newCachedThreadPool(),
372                 new OutcomeReceiver<>() {
373                     @Override
374                     public void onResult(Boolean result) {
375                         responseFuture.set(result);
376                     }
377 
378                     @Override
379                     public void onError(Exception exception) {
380                         responseFuture.setException(exception);
381                     }
382                 });
383 
384         Boolean response = responseFuture.get();
385         assertThat(response).isTrue();
386     }
387 
388     /** Returns a [BySelector] of a resource in sysui package. */
sysuiResSelector(String resourceId)389     public static BySelector sysuiResSelector(String resourceId) {
390         return By.pkg(SYSTEM_UI_NAME).res(SYSTEM_UI_NAME, resourceId);
391     }
392 }
393