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.get().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.get().setLastConfigInfo( 100 extras.getString(EXTRA_KEY_OWNER), extras.getParcelable(method)); 101 break; 102 103 case METHOD_PUT_EXTRAS: 104 ensureExtras(method, extras); 105 TestJournalContainer.get().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 get().getTestJournal(owner); 271 } 272 getTestJournal(String owner)273 private synchronized TestJournal getTestJournal(String owner) { 274 TestJournal info = mContainer.get(owner); 275 if (info == null) { 276 info = new TestJournal(); 277 mContainer.put(owner, info); 278 } 279 return info; 280 } 281 addCallback(String owner, ActivityCallback callback)282 synchronized void addCallback(String owner, ActivityCallback callback) { 283 getTestJournal(owner).callbacks.add(callback); 284 } 285 setLastConfigInfo(String owner, ConfigInfo configInfo)286 synchronized void setLastConfigInfo(String owner, ConfigInfo configInfo) { 287 getTestJournal(owner).lastConfigInfo = configInfo; 288 } 289 putExtras(String owner, Bundle extras)290 synchronized void putExtras(String owner, Bundle extras) { 291 getTestJournal(owner).extras.putAll(extras); 292 } 293 get()294 private synchronized static TestJournalContainer get() { 295 if (!TestJournalProvider.sCrossProcessAccessGuard) { 296 throw new IllegalAccessError(TestJournalProvider.class.getSimpleName() 297 + " is not alive in this process"); 298 } 299 if (sInstance == null) { 300 sInstance = new TestJournalContainer(); 301 } 302 return sInstance; 303 } 304 305 /** 306 * The method should be called when we are only interested in the following events. It 307 * actually clears the previous records. 308 */ 309 @NonNull start()310 public static TestJournalContainer start() { 311 final TestJournalContainer instance = get(); 312 synchronized (instance) { 313 instance.mContainer.clear(); 314 } 315 return instance; 316 } 317 } 318 } 319