The Activity is a core concept of Android apps and fundamentally shapes app architecture and UI design. Therefore, our testing strategy can benefit from understanding and incorporating activities.
An Android app is not a single experience with a set beginning and end, it is a collection of activities which can be entered and left at will. UI designers think of apps in terms of "screens", which is a pretty close analog to activities. An activity provides the developer with a window in which to draw the UI, and each screen of an app typically maps to a single activity. As a user moves from one screen to another, the app launches the next activity from the current one.
Each of the above screens from the Android settings app, including the main menu, is a separate activity.
The movement between activities isn't limited to staying within a single app. A single user session on a phone could start with a social networking app, which opens a web browser, which opens up an email app, which might open the camera app. An app may launch an activity from any other app, passing information along in the process. While an app UI may direct a user through activities in an established flow, nothing prevents activities from being launched outside this flow.
The reason Android is designed this way is so that apps can send information to each other, or supply actions which can be reused. Launching the camera capture activity from your messenger app is much easier and better for the developer ecosystem than requiring each app developer to write their own camera activity. Desktop apps have some semblance of this functionality when it comes to those pesky mailto:
links and the default file-picker.
All activities in an Android app must be declared in the app's AndroidManifest.xml
file. Apps launch their own activities the same as they launch activities in other apps, using the startActivity()
method.
Here's an example of two activities being defined for the Android default camera app in its manifest file:
<activity
android:name="com.android.camera.CameraActivity"
android:clearTaskOnLaunch="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/app_name"
android:launchMode="singleTask"
android:taskAffinity="com.android.camera.CameraActivity"
android:theme="@style/Theme.Camera"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" >
<intent-filter>
<action android:name="android.media.action.STILL_IMAGE_CAMERA" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="com.android.keyguard.layout"
android:resource="@layout/keyguard_widget" />
</activity>
<activity
android:name="com.android.camera.CaptureActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Camera"
android:configChanges="orientation|screenSize|keyboardHidden"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan"
android:visibleToInstantApps="true">
<intent-filter>
<action android:name="android.media.action.IMAGE_CAPTURE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Almost every Android device has a dedicated "back" button. It's kind of strange, no? We are used to web browsers having back buttons, but how did every app developer agree to use the same built-in back button and have it perform the same way? They didn't need to, every time a new activity is launched, the device records the previously running activity, which will be relaunched when the back button is pressed. This and other familiar Android design features stem from the concept of the Activity.
Activities are launched via Intents. There are two types of intents: explicit and implicit. An explicit intent uses the name of the app along with the name of the activity (as declared in the app's manifest file) to launch the intended activity. Explicit intents are used by developers for moving between activities within a single app. You wouldn't want to start an activity in another developer's app using the activity name, because the developer might change their app's internal structure and they didn't build those activities with your interests in mind.
Activities can be launched on connected devices from the commandline using ADB and the am
command (short for Activity Manager). The following is an example of launching the default camera app using an explicit intent. We specify the package name of the camera app and the name of the activity, from the manifest file excerpt above.
adb shell am start -n com.android.camera2/com.android.camera.CaptureActivity
When developers build their activities to be used by other apps, they label their activities so they may be targeted by implicit intents. As the name implies, an implicit intent doesn't specify a particular app or activity to launch, instead it specifies the sort of thing the user would like to do. Activities can be given an action
and category
. Examples of common android intents are :android.intent.action.SEND
, android.intent.action.VIEW
, and android.intent.action.EDIT
. These actions describe the sorts of things a user might want to do. When an app launches an activity using an implicit intent the Android operating system looks through all the activities of all the apps on the device (using their manifest files) and finds the activities which declared that they respond to the action (and category) specified in the intent. If a single activity is found which handles the given action, the activity is launched and the resulting UI shown to the user.
We can launch the camera app using implicit intents this time, specifying the IMAGE_CAPTURE action:
adb shell am start -a android.media.action.IMAGE_CAPTURE
We can also launch the camera app directly to video capture, rather than still.
adb shell am start -a android.media.action.VIDEO_CAMERA
If multiple apps support the same activity, the user is given a choice as to which activity they'd like to handle the intent. This decision is probably familiar to all Android users:
Pictured above, my emulator has multiple apps which respond to a user's attempt to set a home screen wallpaper, which I triggered using the following ADB command:
adb shell am start -a android.intent.action.SET_WALLPAPER
Information is also passed along with an intent. A data
URI can be specified, for example: to open a specific webpage or photo on the device. The form of the URI determines which activities may respond to the intent, just like with actions. This is how "deep linking" is implemented, the "deep links" are just intents which are matched to activities.
This example opens the default web browser to the url provided, since on my emulator the Chrome browser is the only app that responds to the android.intent.action.VIEW
action and also accepts URIs starting with http
:
adb shell am start -a android.intent.action.VIEW -d http://appiumpro.com
The following URI doesn't start with http
like a web url does, but Google Maps will recognize it and open the given coordinates:
adb shell am start -a android.intent.action.VIEW -d "geo:46.457398,-119.407305"
In addition to the provided URI, special "extra" parameters can be added to intents. These are rather particular to each activity and are outlined in the documentation. Here's a quick example which uses the android.intent.action.SEND
, otherwise known as "Share", and sends some optional text which we'd like to share.
adb shell am start -a "android.intent.action.SEND" -e "android.intent.extra.TEXT" "AppiumPro" -t "text/plain"
Great, we know all about Android activities and intents, and have seen how to launch activities and send intents using ADB, but how do we fit this into our mobile test automation tool belt, and how does Appium interact with activities?
You may have seen the following in Appium server logs before:
[debug] [AndroidDriver] Parsing package and activity from app manifest
[ADB] Using apkanalyzer from /Users/jonahss/Library/Android/sdk/tools/bin/apkanalyzer
[debug] [ADB] Starting '/Users/jonahss/Library/Android/sdk/tools/bin/apkanalyzer' with args ["-h","manifest","print","/var/folders/mj/pgmgyh_95rsbs10yl46b73zc0000gn/T/2019112-4784-1duc7h5.tlh8/appium-app.apk"]
[ADB] Package name: 'io.cloudgrey.the_app'
[ADB] Main activity name: 'io.cloudgrey.the_app.MainActivity'
[debug] [AndroidDriver] Parsed package and activity are: io.cloudgrey.the_app/io.cloudgrey.the_app.MainActivity
By default, Appium parses the AndroidManifest.xml file in the .apk
file to find the main activity and package name. Appium then launches the app under test using these parameters.
If you'd like to launch a different activity in your app, you can use the appPackage
and appActivity
desired capabilities. Intents can also be described using the following desired capabilities: intentAction
, intentCategory
, optionalIntentArguments
, intentFlags
.
The optionalIntentArguments
are the "extra" parameters which can be sent with intents, outlined in the last ADB example about sharing text.
intentFlags
are a complex set of extra commands which can alter the way activities are launched. They mostly have to do with how the newly launched activity relates to a task. Tasks are another Android concept, where each task is a collection of Activities. Intent flags are pretty technical and beyond the scope of this article, but you can learn more about them from the Android documentation.
Activities can also be launched mid-test, rather than at the beginning of a session. This can be done using the startActivity
command:
driver.startActivity(new Activity("com.example", "ActivityName"));
A common requirement for mobile automation is having the app under test hit a development API server. Initially, a small team can publish a development build of the app which points to a development API. When the team grows, you may find yourself with multiple environments to target. Rather than building a different app for each environment, parameters such as the API url can be set via extra intent arguments. This way, an Appium test can specify app settings using the optionalIntentArguments
desired capability, and no change needs to be made to the UI of the app.
Most UI tests tend to test the same screen over and over in different ways, trying different form inputs, etc. Sometimes the screen we are testing is deep in the app, requiring many steps such as search and navigation to get to. Rather than run through the first screens of your app over and over, you can use intents to navigate directly to the activity you'd like to test. This can speed up overall test suite execution times.
Not sure which activities and intents your app supports? All activities must be declared in the app's AndroidManifest.xml
file. If you have your .apk
file, you can find the manifest by opening any android project in Android Studio and choosing Build >> Analyze APK ...
from the top navigation bar. Choose your APK file from the file system and then view the AndroidManifest.xml
file. All activities and the intents they respond to will be listed in <activity>
nodes.
The Android documentation for Activities and Intents are well written and pretty easy to understand. I glossed over a lot of app development specifics to get to the content which is relevant to Appium automation. Read these for more detail:
It has come to my attention that these days, the concept of a "single activity app" is gaining popularity among Android developers. Frameworks are being made which build an entire app within a single Activity. This may limit your ability to jump into specific activities in order to speed up test suites.