1 /*
2  * Copyright (C) 2024 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.app.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.app.ActivityManager;
26 import android.app.ApplicationStartInfo;
27 import android.app.Flags;
28 import android.app.Instrumentation;
29 import android.content.BroadcastReceiver;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.pm.PackageManager;
35 import android.os.Bundle;
36 import android.os.Process;
37 import android.os.UserHandle;
38 import android.platform.test.annotations.RequiresFlagsEnabled;
39 import android.util.Log;
40 
41 import androidx.test.ext.junit.runners.AndroidJUnit4;
42 import androidx.test.platform.app.InstrumentationRegistry;
43 
44 import com.android.compatibility.common.util.AmUtils;
45 import com.android.compatibility.common.util.ShellIdentityUtils;
46 import com.android.compatibility.common.util.SystemUtil;
47 
48 import com.google.errorprone.annotations.FormatMethod;
49 
50 import org.junit.After;
51 import org.junit.Before;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Map;
58 
59 @RunWith(AndroidJUnit4.class)
60 public final class ActivityManagerAppStartInfoTest {
61     private static final String TAG = ActivityManagerAppStartInfoTest.class.getSimpleName();
62 
63     // Begin section: keep in sync with {@link ApiTestActivity}
64     private static final String REQUEST_KEY_ACTION = "action";
65     private static final String REQUEST_KEY_TIMESTAMP_KEY_FIRST = "timestamp_key_first";
66     private static final String REQUEST_KEY_TIMESTAMP_VALUE_FIRST = "timestamp_value_first";
67     private static final String REQUEST_KEY_TIMESTAMP_KEY_LAST = "timestamp_key_last";
68     private static final String REQUEST_KEY_TIMESTAMP_VALUE_LAST = "timestamp_value_last";
69 
70     private static final int REQUEST_VALUE_QUERY_START = 1;
71     private static final int REQUEST_VALUE_ADD_TIMESTAMP = 2;
72     private static final int REQUEST_VALUE_LISTENER_ADD_ONE = 3;
73     private static final int REQUEST_VALUE_LISTENER_ADD_MULTIPLE = 4;
74     private static final int REQUEST_VALUE_LISTENER_ADD_REMOVE = 5;
75     private static final int REQUEST_VALUE_CRASH = 6;
76 
77     private static final String REPLY_ACTION_COMPLETE =
78             "com.android.cts.startinfoapp.ACTION_COMPLETE";
79 
80     private static final String REPLY_EXTRA_STATUS_KEY = "status";
81 
82     private static final int REPLY_EXTRA_SUCCESS_VALUE = 1;
83     //private static final int REPLY_EXTRA_FAILURE_VALUE = 2;
84 
85     private static final int REPLY_STATUS_NONE = -1;
86     // End section: keep in sync with {@link ApiTestActivity}
87 
88     private static final String STUB_APK =
89             "/data/local/tmp/cts/content/CtsAppStartInfoApp.apk";
90     private static final String STUB_PACKAGE_NAME = "com.android.cts.startinfoapp";
91     private static final String SIMPLE_ACTIVITY = ".ApiTestActivity";
92 
93     private static final int FIRST_TIMESTAMP_KEY =
94             ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START;
95     private static final int LAST_TIMESTAMP_KEY =
96             ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER;
97 
98     private static final int MAX_WAITS_FOR_START = 20;
99     private static final int WAIT_FOR_START_MS = 400;
100 
101     // Return states of the ResultReceiverFilter.
102     public static final int RESULT_PASS = 1;
103     public static final int RESULT_FAIL = 2;
104     public static final int RESULT_TIMEOUT = 3;
105 
106     private Context mContext;
107     private Instrumentation mInstrumentation;
108     private ActivityManager mActivityManager;
109     private PackageManager mPackageManager;
110 
111     private int mStubPackageUid;
112     private int mCurrentUserId;
113 
114     @Before
setUp()115     public void setUp() throws Exception {
116         mInstrumentation = InstrumentationRegistry.getInstrumentation();
117         mContext = mInstrumentation.getContext();
118         mActivityManager = mContext.getSystemService(ActivityManager.class);
119         mPackageManager = mContext.getPackageManager();
120         mCurrentUserId = UserHandle.getUserId(Process.myUid());
121 
122         executeShellCmd("pm install -r --force-queryable " + STUB_APK);
123 
124         mStubPackageUid = mPackageManager.getPackageUid(STUB_PACKAGE_NAME, 0);
125     }
126 
127     @After
tearDown()128     public void tearDown() throws Exception {
129         executeShellCmd("am force-stop " + STUB_PACKAGE_NAME);
130         mInstrumentation.getUiAutomation().dropShellPermissionIdentity();
131     }
132 
133     @Test
134     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testLauncherStart()135     public void testLauncherStart() throws Exception {
136         clearHistoricalStartInfo();
137 
138         Intent intent =
139                 mPackageManager.getLaunchIntentForPackage(STUB_PACKAGE_NAME);
140         mContext.startActivity(intent);
141 
142         ApplicationStartInfo info = waitForAppStart();
143 
144         verify(info, STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, intent,
145                 ApplicationStartInfo.START_REASON_LAUNCHER,
146                 ApplicationStartInfo.START_TYPE_COLD,
147                 ApplicationStartInfo.LAUNCH_MODE_STANDARD,
148                 ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN);
149 
150         verifyIds(info, 0, mStubPackageUid, mStubPackageUid, mStubPackageUid);
151     }
152 
153     @Test
154     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testActivityStart()155     public void testActivityStart() throws Exception {
156         clearHistoricalStartInfo();
157 
158         executeShellCmd("am start -n " + STUB_PACKAGE_NAME + "/" + STUB_PACKAGE_NAME
159                 + SIMPLE_ACTIVITY);
160 
161         ApplicationStartInfo info = waitForAppStart();
162 
163         Intent intent = new Intent();
164         intent.setComponent(ComponentName.createRelative(STUB_PACKAGE_NAME,
165                 SIMPLE_ACTIVITY));
166         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
167 
168         verify(info, STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, intent,
169                 ApplicationStartInfo.START_REASON_START_ACTIVITY,
170                 ApplicationStartInfo.START_TYPE_COLD,
171                 ApplicationStartInfo.LAUNCH_MODE_STANDARD,
172                 ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN);
173 
174         verifyIds(info, 0, mStubPackageUid, mStubPackageUid, mStubPackageUid);
175     }
176 
177     /** Test that the wasForceStopped state is accurate in force stopped case. */
178     @Test
179     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testWasForceStopped()180     public void testWasForceStopped() throws Exception {
181         clearHistoricalStartInfo();
182 
183         // Start the test app and wait for it to complete
184         executeShellCmd("am start -n " + STUB_PACKAGE_NAME + "/" + STUB_PACKAGE_NAME
185                 + SIMPLE_ACTIVITY);
186         waitForAppStart();
187 
188         // Now force stop the app
189         executeShellCmd("am force-stop " + STUB_PACKAGE_NAME);
190 
191         // Clear records again, we don't want to check the previous one.
192         clearHistoricalStartInfo();
193 
194         // Start the app again
195         executeShellCmd("am start -n " + STUB_PACKAGE_NAME + "/" + STUB_PACKAGE_NAME
196                 + SIMPLE_ACTIVITY);
197 
198         // Obtain the start record and confirm it shows having been force stopped
199         ApplicationStartInfo info = waitForAppStart();
200         assertTrue(info.wasForceStopped());
201     }
202 
203     /** Test that the wasForceStopped state is accurate in not force stopped case. */
204     @Test
205     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testWasNotForceStopped()206     public void testWasNotForceStopped() throws Exception {
207         clearHistoricalStartInfo();
208 
209         // Start the test app and wait for it to complete
210         executeShellCmd("am start -n " + STUB_PACKAGE_NAME + "/" + STUB_PACKAGE_NAME
211                 + SIMPLE_ACTIVITY);
212         waitForAppStart();
213 
214         // Now force stop the app
215         executeShellCmd("am force-stop " + STUB_PACKAGE_NAME);
216 
217         // Clear records again, we don't want to check the previous one here.
218         clearHistoricalStartInfo();
219 
220         // Start the app with flag to immediately exit
221         executeShellCmd("am start -n %s/%s%s --ei %s %d",
222                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
223                 REQUEST_KEY_ACTION, REQUEST_VALUE_CRASH); // action to perform
224         sleep(1000);
225 
226         // Clear records again, we don't want to check the previous one.
227         clearHistoricalStartInfo();
228 
229         // Start the app again
230         executeShellCmd("am start -n " + STUB_PACKAGE_NAME + "/" + STUB_PACKAGE_NAME
231                 + SIMPLE_ACTIVITY);
232 
233         // Obtain the start record and confirm it shows having not been force stopped
234         ApplicationStartInfo info = waitForAppStart();
235         assertFalse(info.wasForceStopped());
236     }
237 
238     /**
239      * Start an app and make sure its record exists, then verify
240      * the record is removed when the app is uninstalled.
241      */
242     @Test
243     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testAppRemoved()244     public void testAppRemoved() throws Exception {
245         testActivityStart();
246 
247         executeShellCmd("pm uninstall " + STUB_PACKAGE_NAME);
248 
249         List<ApplicationStartInfo> list =
250                 ShellIdentityUtils.invokeMethodWithShellPermissions(
251                         STUB_PACKAGE_NAME, 1,
252                         mActivityManager::getExternalHistoricalProcessStartReasons,
253                         android.Manifest.permission.DUMP);
254 
255         assertTrue(list != null && list.size() == 0);
256     }
257 
258     /**
259      * Test querying the startup of the process we're currently in.
260      */
261     @Test
262     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testQueryThisProcess()263     public void testQueryThisProcess() throws Exception {
264         clearHistoricalStartInfo();
265 
266         ResultReceiverFilter receiver = new ResultReceiverFilter(REPLY_ACTION_COMPLETE, 1);
267 
268         // Start the app and have it query its own start info record.
269         executeShellCmd("am start -n %s/%s%s --ei %s %d",
270                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
271                 REQUEST_KEY_ACTION, REQUEST_VALUE_QUERY_START); // action to perform
272 
273         // Wait for complete callback
274         assertEquals(RESULT_PASS, receiver.waitForActivity());
275         receiver.close();
276 
277         // Confirm that the app confirmed that it successfully obtained record.
278         assertEquals(1, receiver.mIntents.size());
279 
280         Bundle extras = receiver.mIntents.get(0).getExtras();
281         assertNotNull(extras);
282 
283         int status = extras.getInt(REPLY_EXTRA_STATUS_KEY, -1);
284         assertEquals(REPLY_EXTRA_SUCCESS_VALUE, status);
285     }
286 
287     /**
288      * Test adding timestamps and verify that the timestamps that were added are still there on a
289      * subsequent query.
290      *
291      * Timestamp is created by test runner process and provided to test app to add to start record
292      * as apps can only add timestamps to their own starts. The subsequent query is performed here
293      * in the test app as querying records can be done from other processes and querying the process
294      * itself is not being tested here.
295      */
296     @Test
297     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testAddingTimestamps()298     public void testAddingTimestamps() throws Exception {
299         clearHistoricalStartInfo();
300 
301         final long timestampFirst = System.nanoTime();
302         final long timestampLast = timestampFirst + 1000L;
303         ResultReceiverFilter receiver = new ResultReceiverFilter(REPLY_ACTION_COMPLETE, 1);
304 
305         // Start the app and have it add the provided timestamp to its start record.
306         executeShellCmd(
307                 "am start -n %s/%s%s --ei %s %d --ei %s %d --el %s %d --ei %s %d --el %s %d",
308                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
309                 REQUEST_KEY_ACTION, REQUEST_VALUE_ADD_TIMESTAMP, // action to perform
310                 REQUEST_KEY_TIMESTAMP_KEY_FIRST, FIRST_TIMESTAMP_KEY, // first timestamp key
311                 REQUEST_KEY_TIMESTAMP_VALUE_FIRST, timestampFirst, // first timestamp value
312                 REQUEST_KEY_TIMESTAMP_KEY_LAST, LAST_TIMESTAMP_KEY, // last timestamp key
313                 REQUEST_KEY_TIMESTAMP_VALUE_LAST, timestampLast); // last timestamp value
314 
315         // Wait for complete callback
316         assertEquals(RESULT_PASS, receiver.waitForActivity());
317         receiver.close();
318 
319         // Get the most recent app start
320         ApplicationStartInfo info = waitForAppStart();
321         assertNotNull(info);
322 
323         // Verify that the timestamps are retrievable and they're the same
324         // when we pull them back out.
325         Map<Integer, Long> timestamps = info.getStartupTimestamps();
326         long timestampFirstFromInfo = timestamps.get(FIRST_TIMESTAMP_KEY);
327         long timestampLastFromInfo = timestamps.get(LAST_TIMESTAMP_KEY);
328 
329         assertNotNull(timestampFirstFromInfo);
330         assertNotNull(timestampLastFromInfo);
331 
332         assertEquals(timestampFirst, timestampFirstFromInfo);
333         assertEquals(timestampLast, timestampLastFromInfo);
334     }
335 
336     /**
337      * Test that registered listeners are triggered when AppStartInfo is complete.
338      */
339     @Test
340     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testTriggerListeners()341     public void testTriggerListeners() throws Exception {
342         clearHistoricalStartInfo();
343 
344         ResultReceiverFilter receiver = new ResultReceiverFilter(REPLY_ACTION_COMPLETE, 1);
345 
346         executeShellCmd("am start -n %s/%s%s --ei %s %d",
347                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
348                 REQUEST_KEY_ACTION, REQUEST_VALUE_LISTENER_ADD_ONE); // action to perform
349 
350         // Wait for complete callback
351         assertEquals(RESULT_PASS, receiver.waitForActivity());
352         receiver.close();
353 
354         // Confirm that the app confirmed that it successfully received a callback.
355         assertEquals(1, receiver.mIntents.size());
356 
357         Bundle extras = receiver.mIntents.get(0).getExtras();
358         assertNotNull(extras);
359 
360         int status = extras.getInt(REPLY_EXTRA_STATUS_KEY, REPLY_STATUS_NONE);
361         assertEquals(REPLY_EXTRA_SUCCESS_VALUE, status);
362     }
363 
364     /**
365      * Test that multiple registered listeners are triggered when AppStartInfo is complete.
366      */
367     @Test
368     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testTriggerMultipleListeners()369     public void testTriggerMultipleListeners() throws Exception {
370         clearHistoricalStartInfo();
371 
372         ResultReceiverFilter receiver = new ResultReceiverFilter(REPLY_ACTION_COMPLETE, 2);
373 
374         executeShellCmd("am start -n %s/%s%s --ei %s %d",
375                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
376                 REQUEST_KEY_ACTION,
377                 REQUEST_VALUE_LISTENER_ADD_MULTIPLE); // action to perform
378 
379         // Wait for complete callback
380         assertEquals(RESULT_PASS, receiver.waitForActivity());
381         receiver.close();
382 
383         // Confirm that the app confirmed that it successfully received a callback.
384         assertEquals(2, receiver.mIntents.size());
385 
386         Bundle extras = receiver.mIntents.get(0).getExtras();
387         assertNotNull(extras);
388 
389         int status = extras.getInt(REPLY_EXTRA_STATUS_KEY, REPLY_STATUS_NONE);
390         assertEquals(REPLY_EXTRA_SUCCESS_VALUE, status);
391 
392         extras = receiver.mIntents.get(1).getExtras();
393         assertNotNull(extras);
394 
395         status = extras.getInt(REPLY_EXTRA_STATUS_KEY, REPLY_STATUS_NONE);
396         assertEquals(REPLY_EXTRA_SUCCESS_VALUE, status);
397     }
398 
399     /**
400      * Test that a removed listener is not triggered when AppStartInfo is complete.
401      */
402     @Test
403     @RequiresFlagsEnabled(Flags.FLAG_APP_START_INFO)
testRemoveListener()404     public void testRemoveListener() throws Exception {
405         clearHistoricalStartInfo();
406 
407         ResultReceiverFilter receiver = new ResultReceiverFilter(REPLY_ACTION_COMPLETE, 2);
408 
409         executeShellCmd("am start -n %s/%s%s --ei %s %d",
410                 STUB_PACKAGE_NAME, STUB_PACKAGE_NAME, SIMPLE_ACTIVITY, // package/activity to start
411                 REQUEST_KEY_ACTION, REQUEST_VALUE_LISTENER_ADD_REMOVE); // action to perform
412 
413         // Wait for timeout callback to ensure the broadcast was only sent once for the remaining
414         // listener. If we get a complete result this means that the removed listener was triggered.
415         assertEquals(RESULT_TIMEOUT, receiver.waitForActivity());
416         receiver.close();
417 
418         // Confirm that the app confirmed that it successfully received a callback on the not
419         // removed listener, and did not receive one on the removed listener.
420         assertEquals(1, receiver.mIntents.size());
421 
422         Bundle extras = receiver.mIntents.get(0).getExtras();
423         assertNotNull(extras);
424 
425         int status = extras.getInt(REPLY_EXTRA_STATUS_KEY, REPLY_STATUS_NONE);
426         assertEquals(REPLY_EXTRA_SUCCESS_VALUE, status);
427     }
428 
clearHistoricalStartInfo()429     private void clearHistoricalStartInfo() throws Exception {
430         executeShellCmd("am clear-start-info --user all " + STUB_PACKAGE_NAME);
431     }
432 
433     /** Query the app start info object until it indicates the startup is complete. */
waitForAppStart()434     private ApplicationStartInfo waitForAppStart() {
435         List<ApplicationStartInfo> list;
436 
437         for (int i = 0; i < MAX_WAITS_FOR_START; i++) {
438             list = ShellIdentityUtils.invokeMethodWithShellPermissions(
439                     STUB_PACKAGE_NAME, 1,
440                     mActivityManager::getExternalHistoricalProcessStartReasons,
441                     android.Manifest.permission.DUMP);
442 
443             if (list != null && list.size() == 1
444                     && list.get(0).getStartupState()
445                         == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
446                 return list.get(0);
447             }
448             sleep(WAIT_FOR_START_MS);
449         }
450 
451         fail("The app didn't finish starting in time.");
452         return null;
453     }
454 
sleep(long timeout)455     private void sleep(long timeout) {
456         try {
457             Thread.sleep(timeout);
458         } catch (InterruptedException e) {
459         }
460     }
461 
462     @FormatMethod
executeShellCmd(String cmdFormat, Object... args)463     private String executeShellCmd(String cmdFormat, Object... args) throws Exception {
464         String cmd = String.format(cmdFormat, args);
465         String result = SystemUtil.runShellCommand(mInstrumentation, cmd);
466         Log.d(TAG, String.format("Output for '%s': %s", cmd, result));
467         return result;
468     }
469 
verifyIds(ApplicationStartInfo info, int pid, int realUid, int packageUid, int definingUid)470     private void verifyIds(ApplicationStartInfo info,
471             int pid, int realUid, int packageUid, int definingUid) {
472         assertNotNull(info);
473 
474         assertEquals(pid, info.getPid());
475         assertEquals(realUid, info.getRealUid());
476         assertEquals(definingUid, info.getDefiningUid());
477         assertEquals(packageUid, info.getPackageUid());
478     }
479 
480     /**
481      * Verify that the info matches the passed state.
482      * Null arguments are skipped in verification.
483      */
verify(ApplicationStartInfo info, String packageName, String processName, Intent intent, int reason, int startType, int launchMode, int startupState)484     private void verify(ApplicationStartInfo info,
485             String packageName, String processName, Intent intent,
486             int reason, int startType, int launchMode, int startupState) {
487         assertNotNull(info);
488 
489         if (packageName != null) {
490             assertTrue(packageName.equals(info.getPackageName()));
491         }
492 
493         if (processName != null) {
494             assertTrue(processName.equals(info.getProcessName()));
495         }
496 
497         if (intent != null) {
498             assertTrue(intent.filterEquals(info.getIntent()));
499         }
500 
501         assertEquals(reason, info.getReason());
502         assertEquals(startType, info.getStartType());
503         assertEquals(launchMode, info.getLaunchMode());
504         assertEquals(startupState, info.getStartupState());
505 
506         // Check that the appropriate timestamps exist based on the startup state
507         // and that they're in the right order.
508         Map<Integer, Long> timestamps = info.getStartupTimestamps();
509         if (startupState == ApplicationStartInfo.STARTUP_STATE_STARTED) {
510             Long launchTimestamp = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_LAUNCH);
511             assertTrue(launchTimestamp != null);
512             assertTrue(launchTimestamp > 0);
513         }
514 
515         if (startupState == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) {
516             Long launchTimestamp = timestamps.get(ApplicationStartInfo.START_TIMESTAMP_LAUNCH);
517             assertTrue(launchTimestamp != null);
518             assertTrue(launchTimestamp > 0);
519 
520             Long bindApplicationTimestamp = timestamps.get(
521                     ApplicationStartInfo.START_TIMESTAMP_BIND_APPLICATION);
522             assertTrue(bindApplicationTimestamp != null);
523             assertTrue(bindApplicationTimestamp > 0);
524 
525             assertTrue(launchTimestamp < bindApplicationTimestamp);
526 
527             // TODO(287153617): Add support for START_TIMESTAMP_APPLICATION_ONCREATE
528             // and START_TIMESTAMP_FIRST_FRAME
529         }
530     }
531 
532     private class ResultReceiverFilter extends BroadcastReceiver {
533         private String mActivityToFilter;
534         private int mResult = RESULT_TIMEOUT;
535         private int mResultsToWaitFor;
536         private static final int TIMEOUT_IN_MS = 5000;
537         List<Intent> mIntents = new ArrayList<Intent>();
538 
539         // Create the filter with the intent to look for.
540         ResultReceiverFilter(String activityToFilter, int resultsToWaitFor) {
541             mActivityToFilter = activityToFilter;
542             mResultsToWaitFor = resultsToWaitFor;
543             IntentFilter filter = new IntentFilter();
544             filter.addAction(mActivityToFilter);
545             mInstrumentation.getTargetContext().registerReceiver(this, filter,
546                     Context.RECEIVER_EXPORTED);
547         }
548 
549         // Turn off the filter.
550         public void close() {
551             mInstrumentation.getTargetContext().unregisterReceiver(this);
552         }
553 
554         @Override
555         public void onReceive(Context context, Intent intent) {
556             if (intent.getAction().equals(mActivityToFilter)) {
557                 synchronized (this) {
558                     mIntents.add(intent);
559                     if (mIntents.size() >= mResultsToWaitFor) {
560                         mResult = RESULT_PASS;
561                         notifyAll();
562                     }
563                 }
564             }
565         }
566 
waitForActivity()567         public int waitForActivity() throws Exception {
568             AmUtils.waitForBroadcastBarrier();
569             synchronized (this) {
570                 try {
571                     wait(TIMEOUT_IN_MS);
572                 } catch (InterruptedException e) {
573                 }
574             }
575             return mResult;
576         }
577     }
578 }
579