Instrumentation testing an Android activity with intents in isolation - android

I am attempting to instrumentation test an Activity in isolation however I'm running into issues because part of the testing requires that I verify that the Activity under test launches another Activity via an Intent.
What I'm looking for is some way to intercept an Intent so that I can verify that the isolated Activity actually attempted to launch the next Activity but without the next Activity actually launching.
The issue I'm running into is that when the next Activity launches it crashes because I'm unable to mock a few critical things that it requires. It would be perfect if there was a way to intercept the Intent during testing so that the next Activity never launches.
Is what I'm looking for even possible?

Originally I tried to use Espresso's intended() and intending() methods in order to verify that Intents were being sent without actually starting an Activity (as described here: https://collectiveidea.com/blog/archives/2015/08/11/stub-your-android-intents
However I did not have luck making that work. What I eventually resorted to was using ActivityMonitor to do the job.
Here's an example:
private void registerActivityMonitorAndStartActivity(String name) {
Instrumentation.ActivityMonitor am = new
Instrumentation.ActivityMonitor(name, null, true);
InstrumentationRegistry.getInstrumentation().addMonitor(am);
mActivityTestRule.launchActivity(new Intent());
int count = 0;
while(!InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1) && count < 50000) {
count++;
}
Timber.d("Count = " + String.valueOf(count));
assertTrue(InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1));
}
This basically has an activity monitor watch for an intent sent to an activity that you specify by name. A while loop runs until the activity monitor sees a hit and then breaks or breaks if a timeout is hit.

Related

Prevent app from being launched at boot if manually launched by user

I've set my application to be launched at system startup and it does so. The problem is that it takes about ~30 seconds between the homescreen showing up and my app to be launched. This may be a problem if the user doesn't wait.
My idea is to prevent the app from being re-launched when the system broadcasts the message IF it is already running.
So, basically, I'm looking for either solution:
Decrease wait time between system start and app start AND / OR;
Prevent app from being launched when the message is received if it is already running.
The first topic is something I want to achieve regardless, but I don't control when android will fire the message. If I could achieve both, it would be great, but I'd settle for the second option.
I don't think it's necessary to show this, but I've set everything in the Manifest (i.e BOOT_COMPLETED) and this is my code, where MyNamedActivity is my main activity (working code):
#Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED) && AppController.getUsuarioLogado() == null) {
Intent i = new Intent(context, MyNamedActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
}
Note: Removing addFlags line causes the app to cash.

How can I check the expected intent sent without actually launching activity in Espresso?

I have a UI test which clicks a button, and then launch a new Activity in its onClickListener. The test checks whether expected intent is sent or not.
My problem is, I want to test whether expected intent is sent without actually launching the activity. Because I found that new activity initializes its state, and it makes subsequent tests flaky.
I know there are two Espresso Intents API, which are intended and intending, but both fail to meet my needs. intended API actually launches the target activity, and intending API doesn't launch the activity, but it calls onActivityResult callback which I don't want either. Because I'm afraid that code inside onActivityResult may cause another flakiness.
Also intending doesn't assert whether matching intent is sent. It just calls onActivityResult callback when matching intent is found, which means I have to check whether onActivityResult is called or not!
Is there a clean way to achieve what I want?
If you want to test whether expected intent is sent without actually launching the activity you can do it by capturing the intent with an activityResult and then catching the activity :
Intent intent = new Intent();
ActivityResult intentResult = new ActivityResult(Activity.RESULT_OK,intent);
intending(anyIntent()).respondWith(intentResult);
onView(withId(R.id.view_id_to_perform_clicking)).check(matches(isDisplayed())).perform(click());
intended(allOf(hasComponent(ActivityToBeOpened.class.getName())));
This would catch any attempt of launching ActivityToBeOpened. If you want to be more specific you can also catch an intent with Extras:
intended(allOf(hasComponent(ActivityToBeOpened.class.getName()), hasExtra("paramName", "value")));
Hope that helps.
Espresso's Intents class is a concise and handy api, but when it doesn't meet your needs, there is an alternative. If you use AndroidJUnit4 test runner, you can get Instrumentaion instance using InstrumentationRegistry.getInstrumentation(), and then you can add Instrumentation.ActivityMonitor instance.
Instrumentation.ActivityMonitor am = new Instrumentation.ActivityMonitor("YOUR_ACTIVITY", null, true);
InstrumentationRegistry.getInstrumentation().addMonitor(am);
onView(withId(R.id.view_id_to_perform_clicking)).check(matches(isDisplayed())).perform(click());
assertTrue(InstrumentationRegistry.getInstrumentation().checkMonitorHit(am, 1));
The third parameter of ActivityMonitor constructor tells we want to block activity launching. Note that this approach has its limitation. In contrast to Espresso Intents' rich Matcher support, You can not set multiple condition for ActivityMonitor.
You can find several samples in ApiDemos, especially in ContactsSelectInstrumentation class.
Actually, you can block any intent to launch an external or your own activity but still use the rich Espresso Intents API:
Instrumentation.ActivityMonitor soloMonitor = solo.getActivityMonitor();
instrumentation.removeMonitor(soloMonitor);
IntentFilter filter = null;
// Block any intent
Instrumentation.ActivityMonitor monitor = instrumentation.addMonitor(filter, null, true);
instrumentation.addMonitor(soloMonitor);
// User action that results in an external browser activity being launched.
user.clickOnView(system.getView(R.id.callButton));
instrumentation.waitForIdleSync();
Intents.intended(Matchers.allOf(
IntentMatchers.hasAction(Matchers.equalTo(Intent.ACTION_VIEW)),
IntentMatchers.hasData(Matchers.equalTo(Uri.parse(url))),
IntentMatchers.toPackage(chromePackage)));
instrumentation.removeMonitor(monitor);
You able to do that because Espresso Intents still records every Intent with IntentMonitor callback even if you block them. Look at the source code of Espresso Intents on how they do that.
If you use Robotium Solo framework you need to move your own ActivityMonitor before their one. Otherwise just skip the lines related to this.

How to detect which activity caused Application to start?

When launching an Activity for an app, the first piece of my code that runs is my subclass of Application.onCreate(). Is there a way to know which Activity triggered that?
More specifically, in my Application subclass onCreate(), I do some database initialization. This can fail and my general solution for failures is to launch another activity where I can display something to the user. This works fine if the failure is anywhere but in Application.onCreate().
When the failure is in Application.onCreate(), Android tries to restart my Application subclass, which in turn fails, and so on. I can prevent the infinite loop with the activity SingleInstance attribute. But that prevents any activity from starting up.
One solution would be to move my database code into my main activity's onStart(). However, I would prefer to leave it in Application.onCreate() if there's a way I can bypass it when the error handling activity is trying to launch.
One approach would be to switch to ACRA for your exception-handling activity, or at least to use their technique.
ACRA winds up in a separate :acra process. They then use ActivityManager and getRunningAppProcesses() to determine if the current process is the :acra process or not:
/**
* #return true if the current process is the process running the SenderService.
* NB this assumes that your SenderService is configured to used the default ':acra' process.
*/
public static boolean isACRASenderServiceProcess(#NonNull Application app) {
final String processName = getCurrentProcessName(app);
if (ACRA.DEV_LOGGING) log.d(LOG_TAG, "ACRA processName='" + processName + "'");
return (processName != null) && processName.equals(ACRA_PRIVATE_PROCESS_NAME);
}
#Nullable
private static String getCurrentProcessName(#NonNull Application app) {
final int processId = android.os.Process.myPid();
final ActivityManager manager = (ActivityManager) app.getSystemService(Context.ACTIVITY_SERVICE);
for (final ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()){
if(processInfo.pid == processId){
return processInfo.processName;
}
}
return null;
}
While getRunningAppProcesses() has been lobotomized in Android 5.0+, you can still use it for your own processes, which is all that we need here.
Given that you know whether you are in the ACRA process or not, you can decide whether or not to do certain initialization, such as your database initialization.
In your case, you would isolate the exception-handling activity in a separate named process, see if you are in that process in Application#onCreate(), and skip the database initialization if you are.
If I understand you correctly, you want to know, WHO started your activity.
And if I also understand correctly, you do start (at least sometimes) this activity from inside your app.
If both assumptions are true, take a look at the Intent class. You start an activity with an intent, where you can put anything in it, with methods like .putString(...) and similar.
So when starting your activity do something like
Intent intent = new Intent(this, myotheractivity.class);
intent.putString("caller", this.getClass().getSimpleName());
startActivity(intent);
And store the name of the calling class (or anything else!) in the activity.
In the onCreate() or your activity just check with a construct like this:
Intent intent = getIntent();
if (intent != null) {
String caller = intent.getString("caller", "");
if (!caller.equals("")) {
// Here caller contains the name of the calling class
}
}
If this intent is null or caller=="", it was not your own app that started this activity.
Cheers

Activity opened twice

I have an application that uses Urban Airship for push notification. When a notification arrives and the user clicks on it, activity A in my application should open and do something.
I've installed the BroadcastReceiver as is shown in the docs, and it's almost working.
When my app is in the foreground I don't let the user see the notification at all, and just handle it automatically.
When my app is not running at all, the activity opens up just fine.
When my app is in the background (which always happens when A is the top activity), a second instance of Activity A is created.
This is, of course, a problem. I don't want two A activities, I just want one of them. Here's the relevant BroadcastReceiver code:
#Override
public void onReceive(Context ctx, Intent intent)
{
Log.i(tag, "Push notification received: " + intent.toString());
String action = intent.getAction();
int notificationId = intent.getIntExtra(PushManager.EXTRA_NOTIFICATION_ID, -1);
if(action.equals(PushManager.ACTION_NOTIFICATION_OPENED))
{
Intent intentActivity = new Intent(ctx, ActivityA.class);
intentActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
UAirship.shared().getApplicationContext().startActivity((intentActivity);
}
}
UPDATE:
I tried to bypass this bug by calling System.exit(0) when the user presses Back on Activity A. The process ended, but then it was restarted immediately! My BroadcastReceiver is not called again in the second instance. What's happening?
UPDATE 2:
#codeMagic asked for more information about the app and activity A.
This app lets its user review certain items and comment on them. Activity A is started when the app is launched. If the user's session isn't valid any more, a Login activity is started. Once the user logs in, activity A becomes active again. A only has a "No items to review" message and a "Try now" button.
When the user logs in, the server starts sending push notifications whenever a new item is available for review. When the app gets the notification, activity A accesses the server and gets the next item to review. The item is shown in activity B. Once the review is submitted to the server, activity B finishes and activity A is again the top activity.
The server knows when a user is reviewing an item (because activity A fetched it), and doesn't send push notifications until the review is submitted - meaning a notification can't come if the user isn't logged in or if the user is viewing activity B.
While I agree there is a subtle race condition here, it is not causing the problem I'm seeing - in testing I am 100% positive there's no race condition - the push notification is only sent after Activity A becomes active again.
The solution was to add a launchMode='singleTask' to the activity in AndroidManifest.xml . As a result, instead of a new activity, onNewIntent of the same activity instance is called.
You can use one of several Intent Flags. FLAG_ACTIVITY_REORDER_TO_FRONT being one of them. This will bring the Activity to the front of the stack if it is already in the stack and if not then it will create a new instance. I believe you will still need FLAG_ACTIVITY_NEW_TASK if you aren't calling it from an Activity
Intent.FLAG_ACTIVITY_CLEAR_TOP should also work. But this will clear any other Activities on the stack. It just depends on what other functionality you need. Look through the Intent Flags and see which of these will work best for you
There are multiple scenarios when this could happen. One of them can be handled this way. Please see my answer here: https://stackoverflow.com/a/44117025/2959575
Ok, two notes on this :
You can register a broadcast receiver via the manifest so it is independent of any parts of your app. and use a Singleton pattern (keep a static reference to your activity somewhere in your app) that way you can check if their is an activity viewing or not and process accordingly.
// your activity A
#Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
myActivityReference = this;
}
public void onPause() {
super.onPause();
if (isFinishing()) {
myActivityReference = null;
}
}
or you can keep everything as it is and use activity lunching modes flags in your manifest such as singleTop, singleInstance ... etc. take a look here android activity lunch modes

Does Android's NFC foreground dispatch system have a bug?

I have an annoying issue with the foreground dispatch behavior. Sometimes instead of calling onNewIntent(), it completely recreates the activity, which breaks the app's workflow.
My concrete situation: Activity A is the MainActivity, which uses the foreground dispatch. Everything works as it should. However, in my activity B, which is launched from the browser (VIEW action), the foreground dispatch doesn't work under some circumstances anymore.
The workflow:
I start the MainActivity, switch to the browser (without closing
the MainActivity), launch activity B and attach my NFC device --> it
creates a new activity B.
I start the MainActivity and close it again. After that I switch
to the browser, launch activity B and attach my NFC device -->
everything works with onNewIntent()
The code is correct, e.g. if I attach the NFC device in the first scenario twice, it works as it should at the second time, but not at the first time. In the MainActivity and activity B I definitively call the disableForegroundDispatch() method in the activity's onPause() method.
Is there a solution for my specific problem? For me it sounds like a bug.
Edit:
public void resume(Activity targetActivity) {
if (nfc != null && nfc.isEnabled()) {
// nfc is the default NFC adapter and never null on my devices
Intent intent = new Intent(targetActivity, targetActivity.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(targetActivity, 0, intent, 0);
nfc.enableForegroundDispatch(targetActivity, pendingIntent, null, new String[][] { new String[] { IsoDep.class.getName() } });
}
}
public void pause(Activity targetActivity) {
if (nfc != null && nfc.isEnabled()) {
nfc.disableForegroundDispatch(targetActivity);
}
}
These methods are called in the corresponding methods in each activity. Thanks for the help!
Solution: After a very long research I finally found the issue. Logcat printed:
startActivity called from non-Activity context; forcing Intent.FLAG_ACTIVITY_NEW_TASK for: Intent
I found other issues at Stackoverflow, where people had have the same issue with the NotificationManager, but all the hints didn't help me. Adding the flag singleTask to my activity B did the trick for me, but to be honest I don't understand it, because the context is always an activity.
I removed all the code from the MainActivity and the first scenario still didn't work. I romved the MainActivity from the manifest and after that everything was fine. Maybe it is a problem, that an app instance is running and activity B is launched from the browser? I don't know.
Anyway, thanks for the help NFC guy!
The IntentFilter[] that you pass to enableForegroundDispatch is empty. So your NFC intent probably arrive at your Activity due to the IntentFilter(s) in the manifest file. This explains the behaviour you observe, as an NFC intent always creates a new instance when delivered this way.
Add something like this instead to your code for enabling foreground dispatch:
IntentFilter[] iFilters = new IntentFilter[2];
iFilters[0] = new IntentFilter();
iFilters[0].addAction("android.nfc.action.TECH_DISCOVERED");
iFilters[1] = new IntentFilter();
iFilters[1].addAction("android.nfc.action.TAG_DISCOVERED");
iFilters[1].addCategory(Intent.CATEGORY_DEFAULT);
And pass that as parameter to enableForegroundDispatch.
UPDATE:
I recently learned more about this specific problem. It is caused by the way Android determines in which task a new Activity should be launched. I don't know or understand the specific details of how that works, but the effect is that:
When Activity B is launched from the Browser, it is created in the Browser's task
When the NFC intent arrives, the system determines that a new Activity B is to be created in Activity A's task
Because of 2., the SINGLE_TOP is not ignored: there is only one instance of Activity B at the top of A's task. When Activity A is closed, it's task has disappeared, so Activity B will always be created in the Browser's task, as you have observed.
You may feel that this is an Android bug in this case (I do, I think), but this behaviour of how to create activities in which task is so fundamental to Android that many apps rely on it. So it is very unlikely that this will ever change.
Possible work-around: declare Activity B with launchMode "singleTask" (or "singleInstance"). Then a new (3rd) task will be created when B is launched.
I guess your workflow is as follow : Main --> Detect tag --> Reader Activity --> Writer activity Detect tag --> Write tag
This problem seems to arise when your writer activity (the one with foreground enabled, I suppose that's for writing purposes) belongs to an Activity Stack (e.g. a Task) that was called from a previous tag discovery.
In particular, it doesn't arise if your workflow is as follow :
Main Writer activity Detect tag --> Write tag
My workaround is to call the writer activity in a new task to begin with.
In the activity that laucnhes the writer, just add the new task flag in the intent that starts the writer.
startActivity(new Intent(this,MyTagWriterActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
Main --> Detect tag --> Reader Activity -NEW_TASK-> Writer activity Detect tag --> Write tag
It does mess with the activity history, but makes the writer activity more predictable.

Categories

Resources