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 
79     /** Avoid accidentally getting data from {@link #TestJournalContainer} in another process. */
80     private static boolean sCrossProcessAccessGuard;
81 
82     @Override
onCreate()83     public boolean onCreate() {
84         sCrossProcessAccessGuard = true;
85         return true;
86     }
87 
88     @Override
call(String method, String arg, Bundle extras)89     public Bundle call(String method, String arg, Bundle extras) {
90         switch (method) {
91             case METHOD_ADD_CALLBACK:
92                 ensureExtras(method, extras);
93                 TestJournalContainer.getInstance().addCallback(
94                         extras.getString(EXTRA_KEY_OWNER), extras.getParcelable(method));
95                 break;
96 
97             case METHOD_SET_LAST_CONFIG_INFO:
98                 ensureExtras(method, extras);
99                 TestJournalContainer.getInstance().setLastConfigInfo(
100                         extras.getString(EXTRA_KEY_OWNER), extras.getParcelable(method));
101                 break;
102 
103             case METHOD_PUT_EXTRAS:
104                 ensureExtras(method, extras);
105                 TestJournalContainer.getInstance().putExtras(
106                         extras.getString(EXTRA_KEY_OWNER), extras);
107                 break;
108         }
109         return null;
110     }
111 
ensureExtras(String method, Bundle extras)112     private static void ensureExtras(String method, Bundle extras) {
113         if (extras == null) {
114             throw new IllegalArgumentException(
115                     "The calling method=" + method + " does not allow null bundle");
116         }
117         extras.setClassLoader(TestJournal.class.getClassLoader());
118         if (DEBUG) {
119             extras.size(); // Trigger unparcel for printing plain text.
120             Log.i(TAG, method + " extras=" + extras);
121         }
122     }
123 
124     /** Records the activity is called with the given callback. */
putActivityCallback(Activity activity, ActivityCallback callback)125     public static void putActivityCallback(Activity activity, ActivityCallback callback) {
126         try (TestJournalClient client = TestJournalClient.create(activity,
127                 activity.getComponentName())) {
128             client.addCallback(callback);
129         }
130     }
131 
132     /** Puts information about the activity. */
putExtras(Activity activity, Consumer<Bundle> bundleFiller)133     public static void putExtras(Activity activity, Consumer<Bundle> bundleFiller) {
134         putExtras(activity, activity.getComponentName(), bundleFiller);
135     }
136 
137     /** Puts information about the component. */
putExtras(Context context, ComponentName owner, Consumer<Bundle> bundleFiller)138     public static void putExtras(Context context, ComponentName owner,
139             Consumer<Bundle> bundleFiller) {
140         putExtras(context, componentNameToKey(owner), bundleFiller);
141     }
142 
143     /** Puts information about the keyword. */
putExtras(Context context, String owner, Consumer<Bundle> bundleFiller)144     public static void putExtras(Context context, String owner, Consumer<Bundle> bundleFiller) {
145         try (TestJournalClient client = TestJournalClient.create(context, owner)) {
146             final Bundle extras = new Bundle();
147             bundleFiller.accept(extras);
148             client.putExtras(extras);
149         }
150     }
151 
152     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)153     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
154             String sortOrder) {
155         return null;
156     }
157 
158     @Override
getType(Uri uri)159     public String getType(Uri uri) {
160         return null;
161     }
162 
163     @Override
insert(Uri uri, ContentValues values)164     public Uri insert(Uri uri, ContentValues values) {
165         return null;
166     }
167 
168     @Override
delete(Uri uri, String selection, String[] selectionArgs)169     public int delete(Uri uri, String selection, String[] selectionArgs) {
170         return 0;
171     }
172 
173     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)174     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
175         return 0;
176     }
177 
componentNameToKey(ComponentName name)178     private static String componentNameToKey(ComponentName name) {
179         return name.flattenToShortString();
180     }
181 
182     /**
183      * Executes from the testing component to put their info to {@link TestJournalProvider}.
184      * The caller can be from any process or package.
185      */
186     public static class TestJournalClient implements AutoCloseable {
187         private static final String EMPTY_ARG = "";
188         private final ContentProviderClient mClient;
189         private final String mOwner;
190 
TestJournalClient(ContentProviderClient client, String owner)191         public TestJournalClient(ContentProviderClient client, String owner) {
192             mClient = client;
193             mOwner = owner;
194         }
195 
callWithExtras(String method, Bundle extras)196         private void callWithExtras(String method, Bundle extras) {
197             extras.putString(EXTRA_KEY_OWNER, mOwner);
198             try {
199                 mClient.call(method, EMPTY_ARG, extras);
200             } catch (RemoteException e) {
201                 throw new RuntimeException(e);
202             }
203         }
204 
addCallback(ActivityCallback callback)205         public void addCallback(ActivityCallback callback) {
206             final Bundle extras = new Bundle();
207             extras.putParcelable(METHOD_ADD_CALLBACK, callback);
208             callWithExtras(METHOD_ADD_CALLBACK, extras);
209         }
210 
setLastConfigInfo(ConfigInfo configInfo)211         public void setLastConfigInfo(ConfigInfo configInfo) {
212             final Bundle extras = new Bundle();
213             extras.putParcelable(METHOD_SET_LAST_CONFIG_INFO, configInfo);
214             callWithExtras(METHOD_SET_LAST_CONFIG_INFO, extras);
215         }
216 
putExtras(Bundle extras)217         public void putExtras(Bundle extras) {
218             callWithExtras(METHOD_PUT_EXTRAS, extras);
219         }
220 
221         @Override
close()222         public void close() {
223             mClient.close();
224         }
225 
226         @NonNull
create(Context context, ComponentName owner)227         static TestJournalClient create(Context context, ComponentName owner) {
228             return create(context, componentNameToKey(owner));
229         }
230 
231         @NonNull
create(Context context, String owner)232         static TestJournalClient create(Context context, String owner) {
233             final ContentProviderClient client = context.getContentResolver()
234                     .acquireContentProviderClient(URI);
235             if (client == null) {
236                 throw new RuntimeException("Unable to acquire " + URI);
237             }
238             return new TestJournalClient(client, owner);
239         }
240     }
241 
242     /** The basic unit to store testing information. */
243     public static class TestJournal {
244         @NonNull
245         public final ArrayList<ActivityCallback> callbacks = new ArrayList<>();
246         @NonNull
247         public final Bundle extras = new Bundle();
248         @Nullable
249         public ConfigInfo lastConfigInfo;
250     }
251 
252     /**
253      * The container lives in test case side. It stores the information from testing components.
254      * The caller must be in the same process as {@link TestJournalProvider}.
255      */
256     public static class TestJournalContainer {
257         private static TestJournalContainer sInstance;
258         private final ArrayMap<String, TestJournal> mContainer = new ArrayMap<>();
259 
TestJournalContainer()260         private TestJournalContainer() {
261         }
262 
263         @NonNull
get(ComponentName owner)264         public static TestJournal get(ComponentName owner) {
265             return get(componentNameToKey(owner));
266         }
267 
268         @NonNull
get(String owner)269         public static TestJournal get(String owner) {
270             return getInstance().getTestJournal(owner);
271         }
272 
273         /**
274          * Perform the action which may have thread safety concerns when accessing the fields of
275          * {@link TestJournal}.
276          */
withThreadSafeAccess(Runnable action)277         public static void withThreadSafeAccess(Runnable action) {
278             synchronized (getInstance()) {
279                 action.run();
280             }
281         }
282 
getTestJournal(String owner)283         private synchronized TestJournal getTestJournal(String owner) {
284             TestJournal info = mContainer.get(owner);
285             if (info == null) {
286                 info = new TestJournal();
287                 mContainer.put(owner, info);
288             }
289             return info;
290         }
291 
addCallback(String owner, ActivityCallback callback)292         synchronized void addCallback(String owner, ActivityCallback callback) {
293             getTestJournal(owner).callbacks.add(callback);
294         }
295 
setLastConfigInfo(String owner, ConfigInfo configInfo)296         synchronized void setLastConfigInfo(String owner, ConfigInfo configInfo) {
297             getTestJournal(owner).lastConfigInfo = configInfo;
298         }
299 
putExtras(String owner, Bundle extras)300         synchronized void putExtras(String owner, Bundle extras) {
301             getTestJournal(owner).extras.putAll(extras);
302         }
303 
getInstance()304         private synchronized static TestJournalContainer getInstance() {
305             if (!TestJournalProvider.sCrossProcessAccessGuard) {
306                 throw new IllegalAccessError(TestJournalProvider.class.getSimpleName()
307                         + " is not alive in this process");
308             }
309             if (sInstance == null) {
310                 sInstance = new TestJournalContainer();
311             }
312             return sInstance;
313         }
314 
315         /**
316          * The method should be called when we are only interested in the following events. It
317          * actually clears the previous records.
318          */
319         @NonNull
start()320         public static TestJournalContainer start() {
321             final TestJournalContainer instance = getInstance();
322             synchronized (instance) {
323                 instance.mContainer.clear();
324             }
325             return instance;
326         }
327     }
328 }
329