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