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 = context.getSystemService(managerClass); 108 // Must handle in a separate thread as some APIs will fail when called from main's 109 Object result = callOnHandlerThread(() -> method.invoke(manager, args)); 110 111 if (VERBOSE) { 112 // Some results - like network logging events - are quite large 113 Log.v(TAG, "runManagerMethod(): method returned " + result); 114 } else { 115 Log.v(TAG, "runManagerMethod(): method returned fine"); 116 } 117 sendResult(receiver, result); 118 } catch (Exception e) { 119 sendError(receiver, e); 120 } 121 122 return true; 123 } 124 125 /** 126 * Called by the device owner {@link DeviceAdminReceiver} to broadcasts an intent to the 127 * receivers in the test case app. 128 * 129 * <p>It must be used in place of standard APIs (such as 130 * {@code LocalBroadcastManager.sendBroadcast()}) because on headless system user mode the test 131 * app might be running in a different user (and this method will take care of IPC'ing the 132 * intent over). 133 */ sendBroadcastToTestAppReceivers(Context context, Intent intent)134 public static void sendBroadcastToTestAppReceivers(Context context, Intent intent) { 135 if (forwardBroadcastToTestApp(context, intent)) return; 136 137 Log.d(TAG, "Broadcasting " + intent.getAction() + " locally on user " 138 + context.getUserId()); 139 LocalBroadcastManager.getInstance(context).sendBroadcast(intent); 140 } 141 142 /** 143 * Forwards the intent to the test app. 144 * 145 * <p>This method is needed in cases where the received of DPM callback must to some processing; 146 * it should try to forward it first, as if it's running on headless system user, the processing 147 * should be tone on the test user side. 148 * 149 * @return when {@code true}, the intent was forwarded and should not be processed locally. 150 */ forwardBroadcastToTestApp(Context context, Intent intent)151 public static boolean forwardBroadcastToTestApp(Context context, Intent intent) { 152 if (!isHeadlessSystemUser()) return false; 153 154 TestAppCallbacksReceiver.sendBroadcast(context, intent); 155 return true; 156 } 157 158 @Nullable findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes)159 private static Method findMethod(Class<?> clazz, String methodName, Class<?>[] parameterTypes) 160 throws NoSuchMethodException { 161 // Handle some special cases first... 162 163 // Methods that use CharSequence instead of String 164 if (parameterTypes != null && parameterTypes.length == 2) { 165 switch (methodName) { 166 case "wipeData": 167 return clazz.getDeclaredMethod(methodName, 168 new Class<?>[] { int.class, CharSequence.class }); 169 case "setDeviceOwnerLockScreenInfo": 170 case "setOrganizationName": 171 return clazz.getDeclaredMethod(methodName, 172 new Class<?>[] { ComponentName.class, CharSequence.class }); 173 } 174 } 175 if ((methodName.equals("setStartUserSessionMessage") 176 || methodName.equals("setEndUserSessionMessage"))) { 177 return clazz.getDeclaredMethod(methodName, 178 new Class<?>[] { ComponentName.class, CharSequence.class }); 179 } 180 181 // Calls with null parameters (and hence the type cannot be inferred) 182 Method method = findMethodWithNullParameterCall(clazz, methodName, parameterTypes); 183 if (method != null) return method; 184 185 // ...otherwise return exactly what as asked 186 return clazz.getDeclaredMethod(methodName, parameterTypes); 187 } 188 189 @Nullable findMethodWithNullParameterCall(Class<?> clazz, String methodName, Class<?>[] parameterTypes)190 private static Method findMethodWithNullParameterCall(Class<?> clazz, String methodName, 191 Class<?>[] parameterTypes) { 192 if (parameterTypes == null) return null; 193 194 Log.d(TAG, "findMethodWithNullParameterCall(): " + clazz + "." + methodName + "(" 195 + Arrays.toString(parameterTypes) + ")"); 196 197 boolean hasNullParameter = false; 198 for (int i = 0; i < parameterTypes.length; i++) { 199 if (parameterTypes[i] == null) { 200 if (VERBOSE) { 201 Log.v(TAG, "Found null parameter at index " + i + " of " + methodName); 202 } 203 hasNullParameter = true; 204 break; 205 } 206 } 207 if (!hasNullParameter) return null; 208 209 List<Method> methods = new ArrayList<>(); 210 for (Method method : clazz.getDeclaredMethods()) { 211 if (method.getName().equals(methodName) 212 && method.getParameterCount() == parameterTypes.length) { 213 methods.add(method); 214 } 215 } 216 if (VERBOSE) Log.v(TAG, "Methods found: " + methods); 217 218 switch (methods.size()) { 219 case 0: 220 return null; 221 case 1: 222 return methods.get(0); 223 default: 224 return findBestMethod(methods, parameterTypes); 225 } 226 } 227 228 @Nullable findBestMethod(List<Method> methods, Class<?>[] parameterTypes)229 private static Method findBestMethod(List<Method> methods, Class<?>[] parameterTypes) { 230 if (VERBOSE) { 231 Log.v(TAG, "Found " + methods.size() + " methods: " + methods); 232 } 233 Method bestMethod = null; 234 235 _methods: for (Method method : methods) { 236 Parameter[] methodParameters = method.getParameters(); 237 for (int i = 0; i < parameterTypes.length; i++) { 238 Class<?> expectedType = parameterTypes[i]; 239 if (expectedType == null) continue; 240 241 Class<?> actualType = methodParameters[i].getType(); 242 if (!expectedType.equals(actualType)) { 243 if (VERBOSE) { 244 Log.v(TAG, "Parameter at index " + i + " doesn't match (expecting " 245 + expectedType + ", got " + actualType + "); rejecting " + method); 246 } 247 continue _methods; 248 } 249 } 250 // double check there isn't more than one 251 if (bestMethod != null) { 252 Log.e(TAG, "found another method (" + method + "), but will use " + bestMethod); 253 } else { 254 bestMethod = method; 255 } 256 } 257 if (VERBOSE) Log.v(TAG, "Returning " + bestMethod); 258 return bestMethod; 259 } 260 sendError(BroadcastReceiver receiver, Exception e)261 private static void sendError(BroadcastReceiver receiver, Exception e) { 262 Log.e(TAG, "Exception handling wrapped DPC call" , e); 263 sendNoLog(receiver, RESULT_EXCEPTION, e); 264 } 265 sendResult(BroadcastReceiver receiver, Object result)266 private static void sendResult(BroadcastReceiver receiver, Object result) { 267 sendNoLog(receiver, RESULT_OK, result); 268 if (VERBOSE) Log.v(TAG, "Sent"); 269 } 270 sendNoLog(BroadcastReceiver receiver, int code, Object result)271 private static void sendNoLog(BroadcastReceiver receiver, int code, Object result) { 272 if (VERBOSE) { 273 Log.v(TAG, "Sending " + TestAppSystemServiceFactory.resultCodeToString(code) 274 + " (result='" + result + "') to " + receiver + " on " 275 + Thread.currentThread()); 276 } 277 receiver.setResultCode(code); 278 if (result != null) { 279 Intent intent = new Intent(); 280 addArg(intent, new Object[] { result }, /* index= */ 0); 281 receiver.setResultExtras(intent.getExtras()); 282 } 283 } 284 DeviceOwnerHelper()285 private DeviceOwnerHelper() { 286 throw new UnsupportedOperationException("contains only static methods"); 287 } 288 } 289