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