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 package com.android.bedstead.dpmwrapper; 17 18 import static com.android.bedstead.dpmwrapper.DataFormatter.addArg; 19 import static com.android.bedstead.dpmwrapper.DataFormatter.getArg; 20 import static com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory.RESULT_EXCEPTION; 21 import static com.android.bedstead.dpmwrapper.TestAppSystemServiceFactory.RESULT_OK; 22 import static com.android.bedstead.dpmwrapper.Utils.ACTION_WRAPPED_MANAGER_CALL; 23 import static com.android.bedstead.dpmwrapper.Utils.EXTRA_CLASS; 24 import static com.android.bedstead.dpmwrapper.Utils.EXTRA_METHOD; 25 import static com.android.bedstead.dpmwrapper.Utils.EXTRA_NUMBER_ARGS; 26 import static com.android.bedstead.dpmwrapper.Utils.VERBOSE; 27 import static com.android.bedstead.dpmwrapper.Utils.callOnHandlerThread; 28 import static com.android.bedstead.dpmwrapper.Utils.isHeadlessSystemUser; 29 30 import android.annotation.Nullable; 31 import android.app.admin.DeviceAdminReceiver; 32 import android.content.BroadcastReceiver; 33 import android.content.ComponentName; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.os.Bundle; 37 import android.util.Log; 38 39 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 40 41 import java.lang.reflect.Method; 42 import java.lang.reflect.Parameter; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 47 /** 48 * Helper class used by the device owner apps. 49 */ 50 public final class DeviceOwnerHelper { 51 52 private static final String TAG = DeviceOwnerHelper.class.getSimpleName(); 53 54 /** 55 * Executes a method requested by the test app. 56 * 57 * <p>Typical usage: 58 * 59 * <pre><code> 60 @Override 61 public void onReceive(Context context, Intent intent) { 62 if (DeviceOwnerAdminReceiverHelper.runManagerMethod(this, context, intent)) return; 63 super.onReceive(context, intent); 64 } 65 </code></pre> 66 * 67 * @return whether the {@code intent} represented a method that was executed. 68 */ runManagerMethod(BroadcastReceiver receiver, Context context, Intent intent)69 public static boolean runManagerMethod(BroadcastReceiver receiver, Context context, 70 Intent intent) { 71 String action = intent.getAction(); 72 Log.d(TAG, "runManagerMethod(): user=" + context.getUserId() + ", action=" + action); 73 74 if (!action.equals(ACTION_WRAPPED_MANAGER_CALL)) { 75 if (VERBOSE) Log.v(TAG, "ignoring, it's not " + ACTION_WRAPPED_MANAGER_CALL); 76 return false; 77 } 78 79 try { 80 String className = intent.getStringExtra(EXTRA_CLASS); 81 String methodName = intent.getStringExtra(EXTRA_METHOD); 82 int numberArgs = intent.getIntExtra(EXTRA_NUMBER_ARGS, 0); 83 Log.d(TAG, "runManagerMethod(): userId=" + context.getUserId() 84 + ", intent=" + intent.getAction() + ", class=" + className 85 + ", methodName=" + methodName + ", numberArgs=" + numberArgs); 86 final Object[] args; 87 Class<?>[] parameterTypes = null; 88 if (numberArgs > 0) { 89 args = new Object[numberArgs]; 90 parameterTypes = new Class<?>[numberArgs]; 91 Bundle extras = intent.getExtras(); 92 for (int i = 0; i < numberArgs; i++) { 93 getArg(extras, args, parameterTypes, i); 94 } 95 Log.d(TAG, "converted args: " + Arrays.toString(args) + " (with types " 96 + Arrays.toString(parameterTypes) + ")"); 97 } else { 98 args = null; 99 } 100 Class<?> managerClass = Class.forName(className); 101 Method method = findMethod(managerClass, methodName, parameterTypes); 102 if (method == null) { 103 sendError(receiver, new IllegalArgumentException( 104 "Could not find method " + methodName + " using reflection")); 105 return true; 106 } 107 Object manager = managerClass.equals(GenericManager.class) 108 ? new GenericManagerImpl(context) 109 : context.getSystemService(managerClass); 110 // Must handle in a separate thread as some APIs will fail when called from main's 111 Object result = callOnHandlerThread(() -> method.invoke(manager, args)); 112 113 if (VERBOSE) { 114 // Some results - like network logging events - are quite large 115 Log.v(TAG, "runManagerMethod(): method returned " + result); 116 } else { 117 Log.v(TAG, "runManagerMethod(): method returned fine"); 118 } 119 sendResult(receiver, result); 120 } catch (Exception e) { 121 sendError(receiver, e); 122 } 123 124 return true; 125 } 126 127 /** 128 * Called by the device owner {@link DeviceAdminReceiver} to broadcasts an intent to the 129 * receivers in the test case app. 130 * 131 * <p>It must be used in place of standard APIs (such as 132 * {@code LocalBroadcastManager.sendBroadcast()}) because on headless system user mode the test 133 * app might be running in a different user (and this method will take care of IPC'ing the 134 * intent over). 135 */ sendBroadcastToTestAppReceivers(Context context, Intent intent)136 public static void sendBroadcastToTestAppReceivers(Context context, Intent intent) { 137 if (forwardBroadcastToTestApp(context, intent)) return; 138 139 Log.d(TAG, "Broadcasting " + intent.getAction() + " locally on user " 140 + context.getUserId()); 141 LocalBroadcastManager.getInstance(context).sendBroadcast(intent); 142 } 143 144 /** 145 * Forwards the intent to the test app. 146 * 147 * <p>This method is needed in cases where the received of DPM callback must to some processing; 148 * it should try to forward it first, as if it's running on headless system user, the processing 149 * should be tone on the test user side. 150 * 151 * @return when {@code true}, the intent was forwarded and should not be processed locally. 152 */ forwardBroadcastToTestApp(Context context, Intent intent)153 public static boolean forwardBroadcastToTestApp(Context context, Intent intent) { 154 if (!isHeadlessSystemUser()) return false; 155 156 TestAppCallbacksReceiver.sendBroadcast(context, intent); 157 return true; 158 } 159 160 @Nullable findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes)161 private static Method findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes) 162 throws NoSuchMethodException { 163 // Handle some special cases first... 164 165 // Methods that use CharSequence instead of String 166 if (parameterTypes != null && parameterTypes.length == 2) { 167 switch (methodName) { 168 case "wipeData": 169 return clazz.getDeclaredMethod(methodName, 170 new Class<?>[] { int.class, CharSequence.class }); 171 case "setDeviceOwnerLockScreenInfo": 172 case "setOrganizationName": 173 return clazz.getDeclaredMethod(methodName, 174 new Class<?>[] { ComponentName.class, CharSequence.class }); 175 } 176 } 177 if ((methodName.equals("setStartUserSessionMessage") 178 || methodName.equals("setEndUserSessionMessage"))) { 179 return clazz.getDeclaredMethod(methodName, 180 new Class<?>[] { ComponentName.class, CharSequence.class }); 181 } 182 183 // Calls with null parameters (and hence the type cannot be inferred) 184 Method method = findMethodWithNullParameterCall(clazz, methodName, parameterTypes); 185 if (method != null) return method; 186 187 // ...otherwise return exactly what as asked 188 return clazz.getDeclaredMethod(methodName, parameterTypes); 189 } 190 191 @Nullable findMethodWithNullParameterCall(Class<?> clazz, String methodName, Class<?>[] parameterTypes)192 private static Method findMethodWithNullParameterCall(Class<?> clazz, String methodName, 193 Class<?>[] parameterTypes) { 194 if (parameterTypes == null) return null; 195 196 Log.d(TAG, "findMethodWithNullParameterCall(): " + clazz + "." + methodName + "(" 197 + Arrays.toString(parameterTypes) + ")"); 198 199 boolean hasNullParameter = false; 200 for (int i = 0; i < parameterTypes.length; i++) { 201 if (parameterTypes[i] == null) { 202 if (VERBOSE) { 203 Log.v(TAG, "Found null parameter at index " + i + " of " + methodName); 204 } 205 hasNullParameter = true; 206 break; 207 } 208 } 209 if (!hasNullParameter) return null; 210 211 List<Method> methods = new ArrayList<>(); 212 for (Method method : clazz.getDeclaredMethods()) { 213 if (method.getName().equals(methodName) 214 && method.getParameterCount() == parameterTypes.length) { 215 methods.add(method); 216 } 217 } 218 if (VERBOSE) Log.v(TAG, "Methods found: " + methods); 219 220 switch (methods.size()) { 221 case 0: 222 return null; 223 case 1: 224 return methods.get(0); 225 default: 226 return findBestMethod(methods, parameterTypes); 227 } 228 } 229 230 @Nullable findBestMethod(List<Method> methods, Class<?>[] parameterTypes)231 private static Method findBestMethod(List<Method> methods, Class<?>[] parameterTypes) { 232 if (VERBOSE) { 233 Log.v(TAG, "Found " + methods.size() + " methods: " + methods); 234 } 235 Method bestMethod = null; 236 237 _methods: for (Method method : methods) { 238 Parameter[] methodParameters = method.getParameters(); 239 for (int i = 0; i < parameterTypes.length; i++) { 240 Class<?> expectedType = parameterTypes[i]; 241 if (expectedType == null) continue; 242 243 Class<?> actualType = methodParameters[i].getType(); 244 if (!expectedType.equals(actualType)) { 245 if (VERBOSE) { 246 Log.v(TAG, "Parameter at index " + i + " doesn't match (expecting " 247 + expectedType + ", got " + actualType + "); rejecting " + method); 248 } 249 continue _methods; 250 } 251 } 252 // double check there isn't more than one 253 if (bestMethod != null) { 254 Log.e(TAG, "found another method (" + method + "), but will use " + bestMethod); 255 } else { 256 bestMethod = method; 257 } 258 } 259 if (VERBOSE) Log.v(TAG, "Returning " + bestMethod); 260 return bestMethod; 261 } 262 sendError(BroadcastReceiver receiver, Exception e)263 private static void sendError(BroadcastReceiver receiver, Exception e) { 264 Log.e(TAG, "Exception handling wrapped DPC call" , e); 265 sendNoLog(receiver, RESULT_EXCEPTION, e); 266 } 267 sendResult(BroadcastReceiver receiver, Object result)268 private static void sendResult(BroadcastReceiver receiver, Object result) { 269 sendNoLog(receiver, RESULT_OK, result); 270 if (VERBOSE) Log.v(TAG, "Sent"); 271 } 272 sendNoLog(BroadcastReceiver receiver, int code, Object result)273 private static void sendNoLog(BroadcastReceiver receiver, int code, Object result) { 274 if (VERBOSE) { 275 Log.v(TAG, "Sending " + TestAppSystemServiceFactory.resultCodeToString(code) 276 + " (result='" + result + "') to " + receiver + " on " 277 + Thread.currentThread()); 278 } 279 receiver.setResultCode(code); 280 if (result != null) { 281 Intent intent = new Intent(); 282 addArg(intent, new Object[] { result }, /* index= */ 0); 283 receiver.setResultExtras(intent.getExtras()); 284 } 285 } 286 DeviceOwnerHelper()287 private DeviceOwnerHelper() { 288 throw new UnsupportedOperationException("contains only static methods"); 289 } 290 } 291