1 /*
2  * Copyright (C) 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.google.android.mobly.snippet.bundled.utils;
18 
19 import android.app.UiAutomation;
20 import android.os.Build;
21 import android.content.Context;
22 import androidx.test.platform.app.InstrumentationRegistry;
23 import com.google.android.mobly.snippet.bundled.SmsSnippet;
24 import com.google.android.mobly.snippet.event.EventCache;
25 import com.google.android.mobly.snippet.event.SnippetEvent;
26 import com.google.android.mobly.snippet.util.Log;
27 import com.google.common.primitives.Primitives;
28 import com.google.common.reflect.TypeToken;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.util.Locale;
32 import java.util.concurrent.LinkedBlockingDeque;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35 
36 public final class Utils {
37 
38     private static final char[] hexArray = "0123456789abcdef".toCharArray();
39 
Utils()40     private Utils() {}
41 
42     /**
43      * Waits util a condition is met.
44      *
45      * <p>This is often used to wait for asynchronous operations to finish and the system to reach a
46      * desired state.
47      *
48      * <p>If the predicate function throws an exception and interrupts the waiting, the exception
49      * will be wrapped in an {@link RuntimeException}.
50      *
51      * @param predicate A lambda function that specifies the condition to wait for. This function
52      *     should return true when the desired state has been reached.
53      * @param timeout The number of seconds to wait for before giving up.
54      * @return true if the operation finished before timeout, false otherwise.
55      */
waitUntil(Utils.Predicate predicate, int timeout)56     public static boolean waitUntil(Utils.Predicate predicate, int timeout) {
57         timeout *= 10;
58         try {
59             while (!predicate.waitCondition() && timeout >= 0) {
60                 Thread.sleep(100);
61                 timeout -= 1;
62             }
63             if (predicate.waitCondition()) {
64                 return true;
65             }
66         } catch (Throwable e) {
67             throw new RuntimeException(e);
68         }
69         return false;
70     }
71 
72     /**
73      * Wait on a specific snippet event.
74      *
75      * <p>This allows a snippet to wait on another SnippetEvent as long as they know the name and
76      * callback id. Commonly used to make async calls synchronous, see {@link
77      * SmsSnippet#waitForSms()} waitForSms} for example usage.
78      *
79      * @param callbackId String callbackId that we want to wait on.
80      * @param eventName String event name that we are waiting on.
81      * @param timeout int timeout in milliseconds for how long it will wait for the event.
82      * @return SnippetEvent if one was received.
83      * @throws Throwable if interrupted while polling for event completion. Throws TimeoutException
84      *     if no snippet event is received.
85      */
waitForSnippetEvent( String callbackId, String eventName, Integer timeout)86     public static SnippetEvent waitForSnippetEvent(
87             String callbackId, String eventName, Integer timeout) throws Throwable {
88         String qId = EventCache.getQueueId(callbackId, eventName);
89         LinkedBlockingDeque<SnippetEvent> q = EventCache.getInstance().getEventDeque(qId);
90         SnippetEvent result;
91         try {
92             result = q.pollFirst(timeout, TimeUnit.MILLISECONDS);
93         } catch (InterruptedException e) {
94             throw e.getCause();
95         }
96 
97         if (result == null) {
98             throw new TimeoutException(
99                     String.format(
100                             Locale.ROOT,
101                             "Timed out waiting(%d millis) for SnippetEvent: %s",
102                             timeout,
103                             callbackId));
104         }
105         return result;
106     }
107 
108     /**
109      * A function interface that is used by lambda functions signaling an async operation is still
110      * going on.
111      */
112     public interface Predicate {
waitCondition()113         boolean waitCondition() throws Throwable;
114     }
115 
116     /**
117      * Simplified API to invoke an instance method by reflection.
118      *
119      * <p>Sample usage:
120      *
121      * <pre>
122      *   boolean result = (boolean) Utils.invokeByReflection(
123      *           mWifiManager,
124      *           "setWifiApEnabled", null /* wifiConfiguration * /, true /* enabled * /);
125      * </pre>
126      *
127      * @param instance Instance of object defining the method to call.
128      * @param methodName Name of the method to call. Can be inherited.
129      * @param args Variadic array of arguments to supply to the method. Their types will be used to
130      *     locate a suitable method to call. Subtypes, primitive types, boxed types, and {@code
131      *     null} arguments are properly handled.
132      * @return The return value of the method, or {@code null} if no return value.
133      * @throws NoSuchMethodException If no suitable method could be found.
134      * @throws Throwable The exception raised by the method, if any.
135      */
invokeByReflection(Object instance, String methodName, Object... args)136     public static Object invokeByReflection(Object instance, String methodName, Object... args)
137             throws Throwable {
138         // Java doesn't know if invokeByReflection(instance, name, null) means that the array is
139         // null or that it's a non-null array containing a single null element. We mean the latter.
140         // Silly Java.
141         if (args == null) {
142             args = new Object[] {null};
143         }
144         // Can't use Class#getMethod(Class<?>...) because it expects that the passed in classes
145         // exactly match the parameters of the method, and doesn't handle superclasses.
146         Method method = null;
147         METHOD_SEARCHER:
148         for (Method candidateMethod : instance.getClass().getMethods()) {
149             // getMethods() returns only public methods, so we don't need to worry about checking
150             // whether the method is accessible.
151             if (!candidateMethod.getName().equals(methodName)) {
152                 continue;
153             }
154             Class<?>[] declaredParams = candidateMethod.getParameterTypes();
155             if (declaredParams.length != args.length) {
156                 continue;
157             }
158             for (int i = 0; i < declaredParams.length; i++) {
159                 if (args[i] == null) {
160                     // Null is assignable to anything except primitives.
161                     if (declaredParams[i].isPrimitive()) {
162                         continue METHOD_SEARCHER;
163                     }
164                 } else {
165                     // Allow autoboxing during reflection by wrapping primitives.
166                     Class<?> declaredClass = Primitives.wrap(declaredParams[i]);
167                     Class<?> actualClass = Primitives.wrap(args[i].getClass());
168                     TypeToken<?> declaredParamType = TypeToken.of(declaredClass);
169                     TypeToken<?> actualParamType = TypeToken.of(actualClass);
170                     if (!declaredParamType.isSupertypeOf(actualParamType)) {
171                         continue METHOD_SEARCHER;
172                     }
173                 }
174             }
175             method = candidateMethod;
176             break;
177         }
178         if (method == null) {
179             StringBuilder methodString =
180                     new StringBuilder(instance.getClass().getName())
181                             .append('#')
182                             .append(methodName)
183                             .append('(');
184             for (int i = 0; i < args.length - 1; i++) {
185                 methodString.append(args[i].getClass().getSimpleName()).append(", ");
186             }
187             if (args.length > 0) {
188                 methodString.append(args[args.length - 1].getClass().getSimpleName());
189             }
190             methodString.append(')');
191             throw new NoSuchMethodException(methodString.toString());
192         }
193         try {
194             Object result = method.invoke(instance, args);
195             return result;
196         } catch (InvocationTargetException e) {
197             throw e.getCause();
198         }
199     }
200 
201     /**
202      * Convert a byte array (binary data) to a hexadecimal string (ASCII) representation.
203      *
204      * <p>[\x01\x02] -&gt; "0102"
205      *
206      * @param bytes The array of byte to convert.
207      * @return a String with the ASCII hex representation.
208      */
bytesToHexString(byte[] bytes)209     public static String bytesToHexString(byte[] bytes) {
210         char[] hexChars = new char[bytes.length * 2];
211         for (int j = 0; j < bytes.length; j++) {
212             int v = bytes[j] & 0xFF;
213             hexChars[j * 2] = hexArray[v >>> 4];
214             hexChars[j * 2 + 1] = hexArray[v & 0x0F];
215         }
216         return new String(hexChars);
217     }
218 
adaptShellPermissionIfRequired(Context context)219    public static void adaptShellPermissionIfRequired(Context context) throws Throwable {
220       if (Build.VERSION.SDK_INT >= 29) {
221         Log.d("Elevating permission require to enable support for privileged operation in Android Q+");
222         UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation();
223         uia.adoptShellPermissionIdentity();
224         try {
225           Class<?> cls = Class.forName("android.app.UiAutomation");
226           Method destroyMethod = cls.getDeclaredMethod("destroy");
227           destroyMethod.invoke(uia);
228         } catch (NoSuchMethodException
229             | IllegalAccessException
230             | ClassNotFoundException
231             | InvocationTargetException e) {
232           throw new RuntimeException("Failed to cleaup Ui Automation", e);
233         }
234       }
235     }
236 }
237