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.common;
17 
18 import com.android.adservices.common.annotations.RequiresAndroidServiceAvailable;
19 import com.android.adservices.shared.testing.AndroidDevicePropertiesHelper;
20 import com.android.adservices.shared.testing.DeviceConditionsViolatedException;
21 import com.android.adservices.shared.testing.Logger;
22 import com.android.adservices.shared.testing.Logger.RealLogger;
23 import com.android.adservices.shared.testing.Nullable;
24 import com.android.adservices.shared.testing.ScreenSize;
25 import com.android.adservices.shared.testing.annotations.RequiresGoDevice;
26 import com.android.adservices.shared.testing.annotations.RequiresLowRamDevice;
27 import com.android.adservices.shared.testing.annotations.RequiresScreenSizeDevice;
28 
29 import com.google.common.annotations.VisibleForTesting;
30 
31 import org.junit.AssumptionViolatedException;
32 import org.junit.rules.TestRule;
33 import org.junit.runner.Description;
34 import org.junit.runners.model.Statement;
35 
36 import java.lang.annotation.Annotation;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.Objects;
41 
42 // NOTE: this class is used by device and host side, so it cannot have any Android dependency
43 /**
44  * Rule used to properly check a test behavior depending on whether the device supports {@code
45  * AdService}.
46  *
47  * <p>Typical usage:
48  *
49  * <pre class="prettyprint">
50  * &#064;Rule
51  * public final AdServicesDeviceSupportedRule adServicesDeviceSupportedRule =
52  *     new AdServicesDeviceSupportedRule();
53  * </pre>
54  *
55  * <p>In the example above, it assumes that every test should only be executed when the device
56  * supports {@code AdServices} - if the device doesn't support it, the test will be skipped (with an
57  * {@link AssumptionViolatedException}).
58  *
59  * <p>This rule can also be used to run tests only on devices that have {@link
60  * android.content.pm.PackageManager#FEATURE_RAM_LOW low memory}, by annotating them with {@link
61  * RequiresLowRamDevice}.
62  *
63  * <p>This rule can also be used to run tests only on devices that have certain screen size, by
64  * annotating them with {@link RequiresScreenSizeDevice}.
65  *
66  * <p>When used with another similar rules, you should organize them using the order of feature
67  * dependency. For example, if the test also requires a given SDK level, you should check use that
68  * rule first, as the device's SDK level is immutable (while whether or not {@code AdServices}
69  * supports a device depends on the device). Example:
70  *
71  * <pre class="prettyprint">
72  * &#064;Rule(order = 0)
73  * public final SdkLevelSupportRule sdkLevelRule = SdkLevelSupportRule.forAtLeastS();
74  *
75  * &#064;Rule(order = 1)
76  * public final AdServicesDeviceSupportedRule adServicesDeviceSupportedRule =
77  *     new AdServicesDeviceSupportedRule();
78  * </pre>
79  *
80  * <p><b>NOTE: </b>this class should NOT be used as {@code ClassRule}, as it would result in a "no
81  * tests run" scenario if it throws a {@link AssumptionViolatedException}.
82  */
83 public abstract class AbstractAdServicesDeviceSupportedRule implements TestRule {
84 
85     protected final Logger mLog;
86     private final AbstractDeviceSupportHelper mDeviceSupportHelper;
87 
88     @VisibleForTesting
89     static final String REQUIRES_LOW_RAM_ASSUMPTION_FAILED_ERROR_MESSAGE =
90             "Test annotated with @RequiresLowRamDevice and device is not.";
91 
92     @VisibleForTesting
93     static final String REQUIRES_SCREEN_SIZE_ASSUMPTION_FAILED_ERROR_MESSAGE =
94             "Test annotated with @RequiresScreenSizeDevice(size=%s) and device is not.";
95 
96     @VisibleForTesting
97     static final String REQUIRES_GO_DEVICE_ASSUMPTION_FAILED_ERROR_MESSAGE =
98             "Test annotated with @RequiresGoDevice and device is not a Go device.";
99 
100     @VisibleForTesting
101     static final String REQUIRES_ANDROID_SERVICE_ASSUMPTION_FAILED_ERROR_MSG =
102             "Test annotated with @RequiresAndroidServiceAvailable and device doesn't have the"
103                     + " android service %s";
104 
105     /** Default constructor. */
AbstractAdServicesDeviceSupportedRule( RealLogger logger, AbstractDeviceSupportHelper deviceSupportHelper)106     public AbstractAdServicesDeviceSupportedRule(
107             RealLogger logger, AbstractDeviceSupportHelper deviceSupportHelper) {
108         mLog = new Logger(Objects.requireNonNull(logger), getClass());
109         mDeviceSupportHelper = Objects.requireNonNull(deviceSupportHelper);
110         mLog.d("Constructor: logger=%s", logger);
111     }
112 
113     /** Checks whether {@code AdServices} is supported by the device. */
isAdServicesSupportedOnDevice()114     public final boolean isAdServicesSupportedOnDevice() {
115         boolean isSupported = mDeviceSupportHelper.isDeviceSupported();
116         mLog.v("isAdServicesSupportedOnDevice(): %b", isSupported);
117         return isSupported;
118     }
119 
120     /** Checks whether the device has low ram. */
isLowRamDevice()121     public final boolean isLowRamDevice() {
122         boolean isLowRamDevice = mDeviceSupportHelper.isLowRamDevice();
123         mLog.v("isLowRamDevice(): %b", isLowRamDevice);
124         return isLowRamDevice;
125     }
126 
127     /** Checks whether the device has large screen. */
isLargeScreenDevice()128     public final boolean isLargeScreenDevice() {
129         boolean isLargeScreenDevice = mDeviceSupportHelper.isLargeScreenDevice();
130         mLog.v("isLargeScreenDevice(): %b", isLargeScreenDevice);
131         return isLargeScreenDevice;
132     }
133 
134     /** Checks whether the device is a go device. */
isGoDevice()135     public final boolean isGoDevice() {
136         boolean isGoDevice = mDeviceSupportHelper.isGoDevice();
137         mLog.v("isGoDevice(): %b", isGoDevice);
138         return isGoDevice;
139     }
140 
141     /**
142      * Check whether the device has a service.
143      *
144      * @return {@code true} when it has and only has one service.
145      */
isAndroidServiceAvailable(String intentAction)146     public final boolean isAndroidServiceAvailable(String intentAction) {
147         boolean isAndroidServiceAvailable =
148                 mDeviceSupportHelper.isAndroidServiceAvailable(intentAction);
149         mLog.v(
150                 "isAndroidServiceAvailable() for Intent action %s: %b",
151                 intentAction, isAndroidServiceAvailable);
152         return isAndroidServiceAvailable;
153     }
154 
155     @Override
apply(Statement base, Description description)156     public Statement apply(Statement base, Description description) {
157         if (!description.isTest()) {
158             throw new IllegalStateException(
159                     "This rule can only be applied to individual tests, it cannot be used as"
160                             + " @ClassRule or in a test suite");
161         }
162         return new Statement() {
163             @Override
164             public void evaluate() throws Throwable {
165                 String testName = description.getDisplayName();
166                 boolean isDeviceSupported = isAdServicesSupportedOnDevice();
167                 boolean isLowRamDevice = isLowRamDevice();
168                 boolean isLargeScreenDevice = isLargeScreenDevice();
169                 boolean isGoDevice = isGoDevice();
170                 RequiresLowRamDevice requiresLowRamDevice =
171                         description.getAnnotation(RequiresLowRamDevice.class);
172                 ScreenSize requiresScreenDevice = getRequiresScreenDevice(description);
173                 RequiresGoDevice requiresGoDevice =
174                         description.getAnnotation(RequiresGoDevice.class);
175                 RequiresAndroidServiceAvailable requiresAndroidServiceAvailable =
176                         getRequiresAndroidServiceAvailable(description);
177 
178                 mLog.d(
179                         "apply(): testName=%s, isDeviceSupported=%b, isLowRamDevice=%b,"
180                                 + " requiresLowRamDevice=%s, requiresAndroidServiceAvailable=%s",
181                         testName,
182                         isDeviceSupported,
183                         isLowRamDevice,
184                         requiresLowRamDevice,
185                         requiresAndroidServiceAvailable);
186                 List<String> assumptionViolatedReasons = new ArrayList<>();
187 
188                 if (!isDeviceSupported
189                         && requiresLowRamDevice == null
190                         && requiresGoDevice == null
191                         && requiresScreenDevice == null) {
192                     // Low-ram devices is a sub-set of unsupported, hence we cannot skip it right
193                     // away as the test might be annotated with @RequiresLowRamDevice (which is
194                     // checked below)
195                     assumptionViolatedReasons.add("Device doesn't support Adservices");
196                 } else {
197                     if (!isLowRamDevice && requiresLowRamDevice != null) {
198                         assumptionViolatedReasons.add(
199                                 REQUIRES_LOW_RAM_ASSUMPTION_FAILED_ERROR_MESSAGE);
200                     }
201                     if (!isGoDevice && requiresGoDevice != null) {
202                         assumptionViolatedReasons.add(
203                                 REQUIRES_GO_DEVICE_ASSUMPTION_FAILED_ERROR_MESSAGE);
204                     }
205                     if (requiresScreenDevice != null
206                             && !AndroidDevicePropertiesHelper.matchScreenSize(
207                                     requiresScreenDevice, isLargeScreenDevice)) {
208                         assumptionViolatedReasons.add(
209                                 String.format(
210                                         REQUIRES_SCREEN_SIZE_ASSUMPTION_FAILED_ERROR_MESSAGE,
211                                         requiresScreenDevice));
212                     }
213                     if (requiresAndroidServiceAvailable != null) {
214                         String intentAction = requiresAndroidServiceAvailable.intentAction();
215                         if (!isAndroidServiceAvailable(intentAction)) {
216                             assumptionViolatedReasons.add(
217                                     String.format(
218                                             Locale.ENGLISH,
219                                             REQUIRES_ANDROID_SERVICE_ASSUMPTION_FAILED_ERROR_MSG,
220                                             intentAction));
221                         }
222                     }
223                 }
224 
225                 // Throw exception in case any of the assumption was violated.
226                 if (!assumptionViolatedReasons.isEmpty()) {
227                     throw new DeviceConditionsViolatedException(assumptionViolatedReasons);
228                 }
229 
230                 base.evaluate();
231             }
232         };
233     }
234 
235     @Nullable
236     private ScreenSize getRequiresScreenDevice(Description description) {
237         RequiresScreenSizeDevice requiresLargeScreenDevice =
238                 description.getAnnotation(RequiresScreenSizeDevice.class);
239         if (requiresLargeScreenDevice != null) {
240             return requiresLargeScreenDevice.value();
241         }
242         return null;
243     }
244 
245     // Check both class and the method for the annotation RequiresAndroidServiceAvailable, while the
246     // method's annotation prevails.
247     @Nullable
248     private RequiresAndroidServiceAvailable getRequiresAndroidServiceAvailable(
249             Description description) {
250         Annotation[] annotations = description.getTestClass().getAnnotations();
251 
252         RequiresAndroidServiceAvailable classAnnotation = null;
253         for (Annotation annotation : annotations) {
254             if (annotation instanceof RequiresAndroidServiceAvailable) {
255                 classAnnotation = (RequiresAndroidServiceAvailable) annotation;
256                 break;
257             }
258         }
259 
260         RequiresAndroidServiceAvailable methodAnnotation =
261                 description.getAnnotation(RequiresAndroidServiceAvailable.class);
262 
263         if (methodAnnotation == null) {
264             return classAnnotation;
265         }
266 
267         return methodAnnotation;
268     }
269 }
270