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] -> "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