1 /*
2  * Copyright (C) 2019 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 
17 package android.server.wm;
18 
19 import android.app.Activity;
20 import android.content.ComponentName;
21 import android.content.ContentProvider;
22 import android.content.ContentProviderClient;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.Bundle;
29 import android.os.RemoteException;
30 import android.server.wm.CommandSession.ActivityCallback;
31 import android.server.wm.CommandSession.ConfigInfo;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 
38 import java.util.ArrayList;
39 import java.util.function.Consumer;
40 
41 /**
42  * Let other testing packages put information for test cases to verify.
43  *
44  * This is a global container that there is no connection between test cases and testing components.
45  * If a precise communication is required, use {@link CommandSession.ActivitySessionClient} instead.
46  *
47  * <p>Sample:</p>
48  * <pre>
49  * // In test case:
50  * void testSomething() {
51  *     TestJournalContainer.start();
52  *     // Launch the testing activity.
53  *     // ...
54  *     assertTrue(TestJournalContainer.get(COMPONENT_NAME_OF_TESTING_ACTIVITY).extras
55  *             .getBoolean("test"));
56  * }
57  *
58  * // In the testing activity:
59  * protected void onResume() {
60  *     super.onResume();
61  *     TestJournalProvider.putExtras(this, bundle -> bundle.putBoolean("test", true));
62  * }
63  * </pre>
64  */
65 public class TestJournalProvider extends ContentProvider {
66     private static final boolean DEBUG = "eng".equals(Build.TYPE);
67     private static final String TAG = TestJournalProvider.class.getSimpleName();
68     private static final Uri URI = Uri.parse("content://android.server.wm.testjournalprovider");
69 
70     /** Indicates who owns the events. */
71     private static final String EXTRA_KEY_OWNER = "key_owner";
72     /** Puts a {@link ActivityCallback} into the journal container for who receives the callback. */
73     private static final String METHOD_ADD_CALLBACK = "add_callback";
74     /** Sets the {@link ConfigInfo} for who reports the configuration info. */
75     private static final String METHOD_SET_LAST_CONFIG_INFO = "set_last_config_info";
76     /** Puts any additional information. */
77     private static final String METHOD_PUT_EXTRAS = "put_extras";
78     /** For test app to put resident data from test case. */
79     private static final String METHOD_PUT_RESIDENT_EXTRAS = "put_resident_extras";
80     /** For test app to get resident data from test case. */
81     private static final String METHOD_GET_RESIDENT_EXTRAS = "get_resident_extras";
82 
83     /** Avoid accidentally getting data from {@link #TestJournalContainer} in another process. */
84     private static boolean sCrossProcessAccessGuard;
85 
86     @Override
onCreate()87     public boolean onCreate() {
88         sCrossProcessAccessGuard = true;
89         return true;
90     }
91 
92     @Override
call(String method, String arg, Bundle extras)93     public Bundle call(String method, String arg, Bundle extras) {
94         switch (method) {
95             case METHOD_ADD_CALLBACK:
96                 ensureExtras(method, extras);
97                 TestJournalContainer.getInstance().addCallback(
98                         extras.getString(EXTRA_KEY_OWNER), extras.getParcelable(method));
99                 break;
100 
101             case METHOD_SET_LAST_CONFIG_INFO:
102                 ensureExtras(method, extras);
103                 TestJournalContainer.getInstance().setLastConfigInfo(
104                         extras.getString(EXTRA_KEY_OWNER), extras.getParcelable(method));
105                 break;
106 
107             case METHOD_PUT_EXTRAS:
108                 ensureExtras(method, extras);
109                 TestJournalContainer.getInstance().putExtras(
110                         extras.getString(EXTRA_KEY_OWNER), extras);
111                 break;
112 
113             case METHOD_PUT_RESIDENT_EXTRAS:
114                 ensureExtras(method, extras);
115                 TestJournalContainer.getInstance().mResidentData.put(arg, extras);
116                 break;
117 
118             case METHOD_GET_RESIDENT_EXTRAS:
119                 return TestJournalContainer.getInstance().mResidentData.get(arg);
120         }
121         return null;
122     }
123 
ensureExtras(String method, Bundle extras)124     private static void ensureExtras(String method, Bundle extras) {
125         if (extras == null) {
126             throw new IllegalArgumentException(
127                     "The calling method=" + method + " does not allow null bundle");
128         }
129         extras.setClassLoader(TestJournal.class.getClassLoader());
130         if (DEBUG) {
131             extras.size(); // Trigger unparcel for printing plain text.
132             Log.i(TAG, method + " extras=" + extras);
133         }
134     }
135 
136     /** Records the activity is called with the given callback. */
putActivityCallback(Activity activity, ActivityCallback callback)137     public static void putActivityCallback(Activity activity, ActivityCallback callback) {
138         try (TestJournalClient client = TestJournalClient.create(activity,
139                 activity.getComponentName())) {
140             client.addCallback(callback);
141         }
142     }
143 
144     /** Puts information about the activity. */
putExtras(Activity activity, Consumer<Bundle> bundleFiller)145     public static void putExtras(Activity activity, Consumer<Bundle> bundleFiller) {
146         putExtras(activity, activity.getComponentName(), bundleFiller);
147     }
148 
149     /** Puts information about the component. */
putExtras(Context context, ComponentName owner, Consumer<Bundle> bundleFiller)150     public static void putExtras(Context context, ComponentName owner,
151             Consumer<Bundle> bundleFiller) {
152         putExtras(context, componentNameToKey(owner), bundleFiller);
153     }
154 
155     /** Puts information about the keyword. */
putExtras(Context context, String owner, Consumer<Bundle> bundleFiller)156     public static void putExtras(Context context, String owner, Consumer<Bundle> bundleFiller) {
157         try (TestJournalClient client = TestJournalClient.create(context, owner)) {
158             final Bundle extras = new Bundle();
159             bundleFiller.accept(extras);
160             client.putExtras(extras);
161         }
162     }
163 
164     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)165     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
166             String sortOrder) {
167         return null;
168     }
169 
170     @Override
getType(Uri uri)171     public String getType(Uri uri) {
172         return null;
173     }
174 
175     @Override
insert(Uri uri, ContentValues values)176     public Uri insert(Uri uri, ContentValues values) {
177         return null;
178     }
179 
180     @Override
delete(Uri uri, String selection, String[] selectionArgs)181     public int delete(Uri uri, String selection, String[] selectionArgs) {
182         return 0;
183     }
184 
185     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)186     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
187         return 0;
188     }
189 
componentNameToKey(ComponentName name)190     private static String componentNameToKey(ComponentName name) {
191         return name.flattenToShortString();
192     }
193 
194     /**
195      * Executes from the testing component to put their info to {@link TestJournalProvider}.
196      * The caller can be from any process or package.
197      */
198     public static class TestJournalClient implements AutoCloseable {
199         private static final String EMPTY_ARG = "";
200         private final ContentProviderClient mClient;
201         private final String mOwner;
202 
TestJournalClient(ContentProviderClient client, String owner)203         public TestJournalClient(ContentProviderClient client, String owner) {
204             mClient = client;
205             mOwner = owner;
206         }
207 
callWithExtras(String method, Bundle extras)208         private void callWithExtras(String method, Bundle extras) {
209             extras.putString(EXTRA_KEY_OWNER, mOwner);
210             try {
211                 mClient.call(method, EMPTY_ARG, extras);
212             } catch (RemoteException e) {
213                 throw new RuntimeException(e);
214             }
215         }
216 
addCallback(ActivityCallback callback)217         public void addCallback(ActivityCallback callback) {
218             final Bundle extras = new Bundle();
219             extras.putParcelable(METHOD_ADD_CALLBACK, callback);
220             callWithExtras(METHOD_ADD_CALLBACK, extras);
221         }
222 
setLastConfigInfo(ConfigInfo configInfo)223         public void setLastConfigInfo(ConfigInfo configInfo) {
224             final Bundle extras = new Bundle();
225             extras.putParcelable(METHOD_SET_LAST_CONFIG_INFO, configInfo);
226             callWithExtras(METHOD_SET_LAST_CONFIG_INFO, extras);
227         }
228 
putExtras(Bundle extras)229         public void putExtras(Bundle extras) {
230             callWithExtras(METHOD_PUT_EXTRAS, extras);
231         }
232 
233         /** Puts the resident data with customized owner key. */
putResidentExtras(String owner, Bundle extras)234         public void putResidentExtras(String owner, Bundle extras) {
235             try {
236                 mClient.call(METHOD_PUT_RESIDENT_EXTRAS, owner, extras);
237             } catch (RemoteException e) {
238                 throw new RuntimeException(e);
239             }
240         }
241 
242         /** Gets the resident data according to the owner key. */
getResidentExtras(String owner)243         public Bundle getResidentExtras(String owner) {
244             try {
245                 return mClient.call(METHOD_GET_RESIDENT_EXTRAS, owner, null /* extras */);
246             } catch (RemoteException e) {
247                 throw new RuntimeException(e);
248             }
249         }
250 
251         @Override
close()252         public void close() {
253             mClient.close();
254         }
255 
256         @NonNull
create(Context context, ComponentName owner)257         static TestJournalClient create(Context context, ComponentName owner) {
258             return create(context, componentNameToKey(owner));
259         }
260 
261         @NonNull
create(Context context, String owner)262         static TestJournalClient create(Context context, String owner) {
263             final ContentProviderClient client = context.getContentResolver()
264                     .acquireContentProviderClient(URI);
265             if (client == null) {
266                 throw new RuntimeException("Unable to acquire " + URI);
267             }
268             return new TestJournalClient(client, owner);
269         }
270     }
271 
272     /** The basic unit to store testing information. */
273     public static class TestJournal {
274         @NonNull
275         public final ArrayList<ActivityCallback> callbacks = new ArrayList<>();
276         @NonNull
277         public final Bundle extras = new Bundle();
278         @Nullable
279         public ConfigInfo lastConfigInfo;
280     }
281 
282     /**
283      * The container lives in test case side. It stores the information from testing components.
284      * The caller must be in the same process as {@link TestJournalProvider}.
285      */
286     public static class TestJournalContainer {
287         private static TestJournalContainer sInstance;
288         private final ArrayMap<String, TestJournal> mContainer = new ArrayMap<>();
289         /** The data in this container won't be cleared by {@link #start()}. */
290         private final ArrayMap<String, Bundle> mResidentData = new ArrayMap<>();
291 
TestJournalContainer()292         private TestJournalContainer() {
293         }
294 
295         @NonNull
get(ComponentName owner)296         public static TestJournal get(ComponentName owner) {
297             return get(componentNameToKey(owner));
298         }
299 
300         @NonNull
get(String owner)301         public static TestJournal get(String owner) {
302             return getInstance().getTestJournal(owner);
303         }
304 
305         /** Removes and returns the resident data by the owner key. */
306         @Nullable
takeResidentData(String owner)307         public static Bundle takeResidentData(String owner) {
308             return getInstance().mResidentData.remove(owner);
309         }
310 
311         /** Puts the resident data. */
putResidentData(String owner, Bundle extras)312         public static void putResidentData(String owner, Bundle extras) {
313             getInstance().mResidentData.put(owner, extras);
314         }
315 
316         /**
317          * Perform the action which may have thread safety concerns when accessing the fields of
318          * {@link TestJournal}.
319          */
withThreadSafeAccess(Runnable action)320         public static void withThreadSafeAccess(Runnable action) {
321             synchronized (getInstance()) {
322                 action.run();
323             }
324         }
325 
getTestJournal(String owner)326         private synchronized TestJournal getTestJournal(String owner) {
327             TestJournal info = mContainer.get(owner);
328             if (info == null) {
329                 info = new TestJournal();
330                 mContainer.put(owner, info);
331             }
332             return info;
333         }
334 
addCallback(String owner, ActivityCallback callback)335         synchronized void addCallback(String owner, ActivityCallback callback) {
336             getTestJournal(owner).callbacks.add(callback);
337         }
338 
setLastConfigInfo(String owner, ConfigInfo configInfo)339         synchronized void setLastConfigInfo(String owner, ConfigInfo configInfo) {
340             getTestJournal(owner).lastConfigInfo = configInfo;
341         }
342 
putExtras(String owner, Bundle extras)343         synchronized void putExtras(String owner, Bundle extras) {
344             getTestJournal(owner).extras.putAll(extras);
345         }
346 
getInstance()347         private synchronized static TestJournalContainer getInstance() {
348             if (!TestJournalProvider.sCrossProcessAccessGuard) {
349                 throw new IllegalAccessError(TestJournalProvider.class.getSimpleName()
350                         + " is not alive in this process");
351             }
352             if (sInstance == null) {
353                 sInstance = new TestJournalContainer();
354             }
355             return sInstance;
356         }
357 
358         /**
359          * The method should be called when we are only interested in the following events. It
360          * actually clears the previous records.
361          */
362         @NonNull
start()363         public static TestJournalContainer start() {
364             final TestJournalContainer instance = getInstance();
365             synchronized (instance) {
366                 instance.mContainer.clear();
367             }
368             return instance;
369         }
370     }
371 }
372