How to Test Image Upload on Android (Complete Guide)
Image upload is a feature that touches many layers of an Android app: UI, storage, networking, permissions, and backend services. When it fails, users cannot share photos, profile pictures, or documen
Why Image Upload Testing Is Critical
Image upload is a feature that touches many layers of an Android app: UI, storage, networking, permissions, and backend services. When it fails, users cannot share photos, profile pictures, or documents, which often blocks core flows such as account creation, messaging, or e‑commerce checkout. A broken upload can also expose security holes—malicious files may be accepted, or sensitive data may be leaked through improper handling of EXIF metadata. Because the feature relies on external factors (device camera quality, available storage, network latency, MIME type detection), defects frequently appear only after release, under real‑world conditions that unit tests never see. Therefore a disciplined testing strategy that combines manual exploration, automated scripts, and autonomous persona‑driven analysis is essential to catch regressions before they impact users.
Common Ways Image Upload Breaks in Production
Understanding typical failure modes helps focus test effort. The list below aggregates issues observed across multiple Android apps in the wild.
| Failure Category | Typical Symptom | Root Cause |
|---|---|---|
| Permission misuse | Upload button disabled or crashes when denied | App does not handle RESULT_DENIED from runtime permission request |
| Incorrect MIME detection | Backend rejects file with “unsupported type” | App sends application/octet-stream instead of image/jpeg |
| Size limits not enforced | OOM crash or ANR when selecting >10 MB photo | No client‑side size check; backend returns 413 but app ignores |
| Rotation/exif loss | Uploaded image appears sideways or stripped of orientation | Bitmap decoding discards EXIF orientation matrix |
| Storage access scoped failure | FileUriExposedException on Android 10+ | Using file:// URIs instead of content:// from Storage Access Framework |
| Network intercept | Upload stalls, retries indefinitely | Missing timeout or exponential backoff; socket hangs on flaky Wi‑Fi |
| Race condition | Duplicate uploads or missing thumbnails | UI triggers upload before previous request completes; no request deduplication |
| Accessibility block | TalkBack users cannot reach the “Choose file” button | Button lacks content‑description or is hidden behind a overlay |
| Security bypass | Malicious APK or script uploaded as image | Backend relies solely on client‑side extension check |
| Localization glitch | Upload dialog shows English strings in Japanese locale | Hard‑coded strings or missing resource qualifiers |
Each of these categories can be exercised with a combination of manual checks, automated assertions, and exploratory runs that simulate varied user behaviors.
Comprehensive Test Matrix for Image Upload
The matrix below organizes test scenarios by dimension (happy path, error paths, edge cases, accessibility, security) and indicates the recommended verification method (M = manual, A = automated, P = persona‑driven autonomous). Use it as a baseline; add project‑specific rows as needed.
| ID | Scenario | Description | Expected Outcome | Verification |
|---|---|---|---|---|
| H1 | Happy path – gallery pick | User selects a JPEG from gallery, confirms, upload succeeds | 200 OK, thumbnail appears, EXIF preserved | A (Espresso) + M (visual check) |
| H2 | Happy path – camera capture | User takes photo, crops, uploads | Same as H1, image oriented correctly | A (UIAutomator) + M |
| H3 | Happy path – drag‑and‑drop (tablet) | User drags image from file manager into upload zone | Upload proceeds, no crash | P (curious persona) |
| E1 | Denied permission | User denies READ_EXTERNAL_STORAGE when prompted | Permission rationale shown, upload button stays disabled | A (Espresso) |
| E2 | Revoked permission at runtime | Permission granted initially, then revoked via Settings while app in foreground | App shows snackbar, blocks upload, does not crash | P (impatient persona) |
| E3 | Zero‑byte file | User selects an empty file (created via file manager) | Backend returns 400, app shows error toast | A (Appium) |
| E4 | Unsupported MIME | User attempts to upload a .pdf or .zip file | Backend rejects, app displays “invalid file type” | M |
| E5 | File size > limit | User selects a 15 MB photo when limit is 5 MB | Client shows size‑exceeded dialog, no network call | A (Espresso) |
| E6 | No storage space | Device storage < 10 MB, user tries to pick image | Picker fails gracefully, app shows low‑storage warning | P (elderly persona) |
| E7 | Rotation/orientation | User picks a portrait photo taken with camera held sideways | Uploaded image displays upright in UI, EXIF Orientation tag retained | A (Instrumentation test with BitmapFactory) |
| E8 | Network loss mid‑upload | Wi‑Fi turned off after 50 % of bytes sent | App shows retry option, does not leak partial file | A (OkHttp mock web server) |
| E9 | Slow network (3G simulation) | Upload takes >30 s, user navigates away and back | Upload persists, progress bar updates correctly | P (power user) |
| E10 | Concurrent uploads | User rapidly taps upload button 5 times | Only one request sent, others debounced | A (Espresso with IdlingResource) |
| A1 | TalkBack navigation | User enables TalkBack, navigates to upload button via swipe | Button announces “Upload image, button”, double‑tap opens picker | A (AccessibilityTestSuite) |
| A2 | Color contrast | Upload button meets 4.5:1 contrast against background | Verified with contrast analyzer | M (using Android Studio’s Layout Inspector) |
| A3 | Touch target size | Button dimensions ≥48 dp | Verified with UI Automator view hierarchy | A |
| S1 | Extension spoofing | User renames a .exe to .jpg and attempts upload | Backend rejects based on MIME sniffing, not extension | P (adversarial persona) |
| S2 | Metadata exfiltration | Image contains GPS coordinates in EXIF | App strips location before upload (if privacy policy requires) | M (exiftool check) |
| S3 | File injection via content provider | Malicious app sends a content:// URI that points to a private file | App fails to open URI or shows permission error | P (novice persona) |
| S4 | TLS interception | Upload endpoint uses self‑signed cert; app accepts invalid cert | Connection fails, app shows network error | A (OkHttp certificate pinning test) |
| L1 | Locale switch | User changes device language to right‑to‑left (Arabic) mid‑session | Layout mirrors correctly, button remains accessible | P (elderly persona) |
| L2 | Dark mode | System theme set to dark | Upload UI adapts, icons remain visible | M |
| R1 | App upgrade | User upgrades from v1.2 to v1.3 while an upload is pending | Pending upload resumes or is cleared according to spec | P (power user) |
| R2 | Backup/restore | User backs up app data via ADB, restores on new device | Upload history and temporary files are handled correctly | A (adb backup/restore script) |
How to read the matrix
- Happy path (H) verifies core functionality.
- Error paths (E) cover client‑side validation and graceful degradation.
- Accessibility (A) ensures compliance with WCAG 2.1 AA as interpreted for Android.
- Security/Privacy (S) targets malicious input and data leakage.
- Locale/Runtime (L/R) capture configuration changes that often escape unit tests.
You can prioritize rows based on risk: H1/H2, E1/E2/E5/E6, A1‑A3, S1‑S2 are usually high‑impact.
Manual Testing Step‑by‑Step Guide
Manual exploration remains valuable for discovering UX friction, permission flows, and device‑specific quirks. Follow this procedure on a physical device or emulator with Google Play services installed.
- Prepare the test environment
- Install the app under test (APK) via
adb install -r app.apk. - Grant
android.permission.READ_EXTERNAL_STORAGEandandroid.permission.CAMERAmanually via Settings → Apps → [YourApp] → Permissions, then revoke them to test the denial flow. - Clear app data (
adb shell pm clear com.example.app) to start from a clean state. - Connect to a Wi‑Fi network; optionally enable network throttling via Android Studio’s Profiler → Network → Advanced → Set latency 150 ms, bandwidth 1 Mbps to simulate 3G.
- Happy path via gallery
- Launch the app, navigate to the screen containing the image upload button.
- Tap the button; the system picker should appear.
- Choose a JPEG image (≥2 MB) from the gallery.
- Confirm any cropping or rotation dialogs.
- Observe a progress indicator, then verify that the uploaded image appears in the UI (e.g., as a profile picture).
- Check Logcat for network calls:
adb logcat | grep -i "upload"and confirm a 200 response.
- Happy path via camera
- Repeat step 2 but select “Take photo” in the picker.
- Frame a scene, capture, accept the photo.
- Verify that the image orientation matches the device’s native orientation (use an EXIF viewer on the pulled file:
adb pull /sdcard/Pictures/... . && exiftool image.jpg).
- Permission denial flow
- Before launching the picker, go to Settings → Apps → [YourApp] → Permissions and set both storage and camera to Deny.
- Return to the app and tap the upload button.
- Expect a rationale dialog (if you implemented one) or a toast explaining why the picker cannot open.
- The button should remain disabled or show an error; the app must not crash.
- Permission revocation at runtime
- Grant both permissions, then start an upload (pick an image).
- While the picker is open, quickly navigate to Settings → Apps → [YourApp] → Permissions and toggle storage to Deny.
- Return to the app; the picker should close, and the app should display an error or allow the user to retry after re‑granting permission.
- Size limit enforcement
- Use a file manager to create a dummy 12 MB file:
dd if=/dev/zero of=/sdcard/large.jpg bs=1M count=12. - Attempt to upload this file.
- The app should show a client‑side size‑exceeded toast before any network request appears in Logcat.
- Network failure simulation
- Start an upload with a medium‑sized image (~3 MB).
- After a few seconds, enable Airplane mode or disable Wi‑Fi via quick settings.
- Observe that the upload stops, a retry button appears, and no partial file remains in
/cacheor/files.
- Accessibility checks
- Enable TalkBack (
Settings → Accessibility → TalkBack). - Swipe to reach the upload button; listen for the announcement.
- Double‑tap to activate; ensure the picker opens and that focus moves appropriately inside the picker.
- Disable TalkBack and verify that color contrast meets 4.5:1 using the Android Studio Layout Inspector’s “Contrast” tool.
- Security probing
- Rename a harmless APK to
test.jpgand try to upload it via the picker (some file managers allow extension change). - The backend should reject it; the app should show an error like “Invalid file type”.
- Use
exiftoolto embed a GPS tag in a JPEG and verify that the uploaded copy does not contain the tag (if your policy strips location).
- Cleanup
- After each test cycle, clear app data to avoid state leakage:
adb shell pm clear com.example.app. - Remove any test files you created on external storage:
adb shell rm /sdcard/large.jpg.
Manual testing shines when you need to observe subtle UI glitches, verify that error messages are user‑friendly, or confirm that the app behaves correctly under interruptions (incoming call, screen rotation). Combine it with automated checks for regression safety.
Automated Testing on Android: Tools and Techniques
Automated tests give fast feedback on regressions and can be run on every commit. For image upload, you need to interact with the system picker, handle permissions, and assert network behavior. Below are the most effective Android‑specific approaches, each with a ready‑to‑run snippet.
1. Espresso with IdlingResource for Network Calls
Espresso runs in‑process UI tests and is ideal for validating UI state and simple permission flows. Use an IdlingResource that monitors OkHttp or Volley callbacks to know when an upload finishes.
// UploadIdlingResource.kt
class UploadIdlingResource(
private val callback: () -> Unit
) : IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null
private var idle = true
init {
// Assume you have a singleton UploadManager exposing a LiveData<UploadState>
UploadManager.getInstance().uploadState.observeForever { state ->
if (state == UploadState.IN_PROGRESS) {
idle = false
} else if (state == UploadState.COMPLETED || state == UploadState.FAILED) {
idle = true
resourceCallback?.onTransitionToIdle()
}
}
}
override fun getName() = "UploadIdlingResource"
override fun isIdleNow() = idle
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.resourceCallback = callback
}
}
// ImageUploadTest.kt
@RunWith(AndroidJUnit4::class)
class ImageUploadTest {
@get:Rule
val grantPermissionRule = GrantPermissionRule(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
private lateinit var idlingResource: UploadIdlingResource
@Before
fun registerIdling() {
idlingResource = UploadIdlingResource { }
IdlingRegistry.getInstance().register(idlingResource)
}
@After
fun unregisterIdling() {
IdlingRegistry.getInstance().unregister(idlingResource)
}
@Test
fun happyPath_galleryUpload_showsImage() {
// Launch the activity
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
// Click upload button
onView(withId(R.id.btn_upload_image)).perform(click())
// System picker appears – use UiDevice to interact because Espresso cannot
// access system dialogs directly
val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
uiDevice.wait(Until.hasObject(By.text("Gallery")), 5000)
uiDevice.findObject(By.text("Gallery")).click()
// Choose first image in grid
uiDevice.wait(Until.hasObject(By.className("android.widget.ImageView")), 5000)
uiDevice.findObject(By.className("android.widget.ImageView")).click()
// Confirm crop if needed
uiDevice.wait(Until.hasObject(By.text("Save")), 5000)
uiDevice.findObject(By.text("Save")).click()
// Wait for upload to complete via IdlingResource (implicitly handled)
onView(withId(R.id.image_preview)).check(matches(isDisplayed()))
onView(withId(R.id.image_preview)).check(matches(withDrawable(R.drawable.expected_image))) // custom matcher
}
}
Why this works
- The
GrantPermissionRuleautomatically grants runtime permissions before each test. UiDeviceis needed to interact with the system picker, which runs outside your app’s process.- The
UploadIdlingResourceensures Espresso waits for the network call to finish before asserting UI changes.
2. UIAutomator for Robust System Dialog Interaction
UIAutomator can launch arbitrary activities and is better suited for cross‑app flows like picking images from DocumentsUI or handling the camera intent.
// ImageUploadUiAutomatorTest.java
@RunWith(AndroidJUnit4.class)
public class ImageUploadUiAutomatorTest {
private UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
// Clear app data to start fresh
Context ctx = InstrumentationRegistry.getTargetContext();
ctx.deleteDatabase("app.db");
ShellUtils.runCommand("pm clear com.example.app");
}
@Test
public void testCameraUpload_orientationCorrect() throws Exception {
// Launch main activity
Intent intent = new Intent();
intent.setClassName("com.example.app", "com.example.app.MainActivity");
device.executeShellCommand("am start -n " + intent.getComponent().flattenToString());
// Click upload button
device.findObject(new UiSelector().resourceId("com.example.app:id/btn_upload_image"))
.click()
.waitForExists(2000);
// Choose "Take photo"
device.findObject(new UiSelector().text("Take photo")).click();
// Handle camera app – set a simple scene (e.g., rotate device to landscape)
device.setOrientationLeft(); // simulate landscape
Thread.sleep(1500); // give camera time to adjust
device.pressKeyCode(KeyEvent.KEYCODE_CAMERA); // capture
// Confirm
device.findObject(new UiSelector().text("OK")).click();
// Wait for upload completion (poll a TextView that shows "Uploaded")
UiObject2 status = device.wait(Until.findObject(
new UiSelector().resourceId("com.example.app:id/tv_upload_status")), 10000);
assertEquals("Uploaded", status.getText());
// Verify orientation: pull the uploaded file and check EXIF
String path = "/sdcard/Pictures/SusaTest/uploaded.jpg";
device.executeShellCommand("cp " + path + " /data/local/tmp/");
PullFile pull = new PullFile("/data/local/tmp/uploaded.jpg", new File(Environment.getExternalStorageDirectory(),
"uploaded_check.jpg"));
pull.start();
pull.join(5000);
ExifInterface exif = new ExifInterface(new File(Environment.getExternalStorageDirectory(),
"uploaded_check.jpg").getAbsolutePath());
int orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
assertTrue(orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
orientation == ExifInterface.ORIENTATION_ROTATE_270);
}
}
Key points
UiDevice.setOrientationLeft()simulates a device rotation to test how the camera handles orientation metadata.- Pulling the uploaded file lets you inspect EXIF or verify that the file is not corrupted.
- UIAutomator works well for testing inter‑app interactions (camera, file picker) that Espresso cannot reach.
3. Appium for Cross‑Platform Scripts (Optional)
If you maintain both Android and iOS test suites, Appium provides a unified JSON Wire Protocol. The same test can be written in JavaScript, Python, or Java.
# test_image_upload_appium.py
import time
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
from appium.webdriver.common.touch_action import TouchAction
desired_caps = {
"platformName": "Android",
"deviceName": "Pixel_4_API_33",
"appPackage": "com.example.app",
"appActivity": ".MainActivity",
"automationName": "UiAutomator2",
"noReset": True,
}
driver = webdriver.Remote("http://localhost:4723/wd/hub", desired_caps)
def test_upload_from_gallery():
driver.find_element(MobileBy.ID, "btn_upload_image").click()
time.sleep(1)
# Switch to native picker context
driver.start_activity("com.android.documentsui", ".FilesActivity")
time.sleep(1)
# Navigate to Pictures folder and select first image
driver.find_element(MobileBy.ANDROID_UIAUTOMATOR,
'new UiSelector().resourceId("com.android.documentsui:id/icon_thumb")').click()
time.sleep(1)
driver.find_element(MobileBy.ANDROID_UIAUTOMATOR,
'new UiSelector().description("Done")').click()
# Wait for upload completion
WebDriverWait(driver, 20).until(
EC.visibility_of_element_located((MobileBy.ID, "tv_upload_status"))
)
assert driver.find_element(MobileBy.ID, "tv_upload_status").text == "Uploaded"
driver.quit()
Appium shines when you need to run the same script on multiple device farms or integrate with CI pipelines that already host an Appium server.
4. Backend Contract Tests with MockWebServer
To avoid flaky reliance on a real server, enqueue expected responses and verify request bodies (including multipart boundaries). This also lets you assert that the client sends correct MIME types and headers.
// UploadContractTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class UploadContractTest {
private lateinit var mockWebServer: MockWebServer
private lateinit var uploadRepository: UploadRepository
@Before
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
val okHttpClient = OkHttpClient.Builder()
.url(mockWebServer.url("/upload"))
.build()
uploadRepository = UploadRepository(okHttpClient)
}
@After
fun tearDown() = mockWebServer.shutdown()
@Test
fun `upload sends correct multipart with image/jpeg`() {
// Enqueue 200 OK with empty body
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
// Create a temporary JPEG file
val jpegFile = File.createTempFile("test", ".jpg")
jpegFile.writeBytes(byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte(), 0xE0.toByte()))
// Trigger upload
uploadRepository.uploadImage(jpegFile).await()
// Take the recorded request
val recorded = mockWebServer.takeRequest()
assertEquals("POST", recorded.method)
assertTrue(recorded.getHeader("Content-Type").startsWith("multipart/form-data; boundary="))
val body = recorded.body.readUtf8()
assertTrue(body.contains("Content-Disposition: form-data; name=\"file\"; filename=\"test.jpg\""))
assertTrue(body.contains("Content-Type: image/jpeg"))
assertTrue(body.contains("\r\n\r\n")) // ensure file data present
}
}
These contract tests guarantee that the client never drifts from the expected API contract, catching bugs like wrong MIME type or missing authorization header before they reach production.
Autonomous Persona‑Driven Exploration with SUSA
While scripted tests cover anticipated flows, real users exhibit unpredictable behavior—rapid tapping, unusual navigation patterns, or deliberate attempts to misuse features. SUSA (SUSATest) autonomously explores an app using a set of defined personas, each with a distinct behavior model. It can surface image‑upload bugs that scripted tests never consider because they fall outside the pre‑defined action sequences.
How SUSA Works in a Nutshell
- Ingestion – You provide an APK or a URL. SUSA installs the app on a cloud‑hosted Android emulator or a real device lab.
- Persona Engine – Eight built‑in personas (curious, impatient, novice, adversarial, elderly, accessibility, power user, and security tester) each possess a probability distribution over actions such as tap, long‑press, swipe, voice input, permission toggling, and app backgrounding.
- Exploration Loop – SUSA maintains a visited‑state graph (screen + UI hierarchy). From each state it selects an action weighted by the current persona’s profile, executes it, observes the result (new screen, crash, ANR, toast, logcat error), and updates the graph.
- Issue Detection – Built‑in detectors watch for:
- Crash or ANR (via tombstone logs)
- Dead buttons (no state change after interaction)
- Accessibility violations (missing content‑description, contrast failures)
- Security red flags (file URI scheme usage, clear‑text HTTP, excessive permissions)
- UX friction (repeated error dialogs, endless loading spinners)
- Learning – After each run, SUSA records dead ends and successfully completed flows. Subsequent runs bias exploration toward under‑tested areas, increasing coverage over time.
- Artifact Generation – From the explored graph, SUSA auto‑generates regression scripts: Appium for Android UI and Playwright for web views, enabling you to lock‑in the discovered paths.
Applying SUSA to Image Upload
When you point SUSA at an app that contains an image upload button, the following persona‑driven scenarios often emerge automatically:
| Persona | Typical Behavior | Discovered Issue Example |
|---|---|---|
| Curious | Taps every visible element, opens overflow menus, tries long‑press on the upload button | Finds a hidden “Upload from URL” field that lacks validation, leading to SSRF when a malicious URL is supplied |
| Impatient | Rapid double‑tap, quickly backgrounds app, returns after a few seconds | Detects a race condition where the upload button becomes enabled before the previous request finishes, causing duplicate uploads |
| Novice | Follows on‑screen hints, avoids advanced gestures, frequently presses back | Reveals that the back button dismisses the picker without clearing a temporary file, leaving orphaned cache |
| Adversarial | Attempts to inject special characters, tries to upload non‑image files, toggles permissions mid‑flow | Identifies that the app accepts a file renamed .jpg but containing a script, because backend relies solely on extension |
| Elderly | Uses larger touch targets, prefers slower interactions, often enables system font scaling | Shows that the upload button’s hit‑area shrinks when font size is increased, making it hard to tap |
| Accessibility | Relies on TalkBack, uses explore‑by‑touch, expects audible feedback | Detects that the upload button lacks a content‑description, causing TalkBack to announce “unlabeled button” |
| Power user | Uses shortcuts, drags‑and‑drops from file manager, rotates device frequently | Finds that dragging a file from a third‑party file manager onto the upload zone triggers a FileUriExposedException on Android 11 due to improper content:// handling |
| Security tester | Scans for clear‑text traffic, attempts to man‑in‑the‑middle with self‑signed certs, checks for exposed logs | Discovers that the app logs the full multipart payload (including base64‑encoded image) to Logcat, exposing potentially sensitive images |
SUSA’s output includes a detailed report with screenshots, logcat excerpts, and the exact sequence of actions that led to each finding. You can then convert those sequences into automated regression tests (Espresso/UIAutomator) or address the root cause directly.
Integrating SUSA into CI
Add a step that runs the CLI agent against your latest APK:
pip install susatest-agent
susatest run \
--apk path/to/app-release.apk \
--personas curious,impatient,adversarial \
--max-depth 6 \
--output-dir ./susareport
The agent exits with a non‑zero status if any high‑severity issue (crash, ANR, security leak) is detected, causing the build to fail. Over time, you’ll notice the “explored screens” count rise, indicating growing coverage without writing additional test code.
Edge Cases That Only Appear in Production
Even the most thorough test matrix can miss issues that arise from the interplay of device fragmentation, carrier‑specific behavior, or user‑generated content. Below are production‑only edge cases that have caused outages in real apps, together with concrete detection strategies.
| Edge Case | Why It Happens in the Wild | Detection Approach |
|---|---|---|
| OEM‑specific gallery apps | Some manufacturers replace the stock picker with a custom UI that returns a content:// URI lacking a readable file path. Your app may try to open the URI with FileInputStream, causing a FileNotFoundException. | Use a device farm (e.g., Firebase Test Lab) to run the upload flow on a sampling of OEM images (Samsung, Xiaomi, OnePlus). Verify that the URI is opened via ContentResolver.openInputStream(uri). |
| MMS‑compressed images | When users share images via messaging apps, the file may be recompressed to lower quality, stripping EXIF or altering dimensions. Your thumbnail generation logic might assume the original dimensions. | Send an MMS‑compressed JPEG through a carrier simulator, upload, and assert that the displayed image’s aspect ratio matches the server‑returned metadata (width/height fields). |
| VPN or proxy interference | Enterprise networks often enforce SSL inspection, presenting a different certificate chain. If your app uses certificate pinning incorrectly, the upload fails silently. | Configure an emulator to use a HTTP proxy with a custom CA cert, enable network security config that allows user‑installed CAs for debug builds, and verify that the upload either succeeds (if pinning is disabled) or shows a clear error message. |
| Background location upload restriction | Android 12+ restricts background access to precise location; if your app extracts GPS from EXIF and tries to send it while in the background, the system may drop the request. | Simulate a background upload (press Home, then trigger upload via a notification action) and check Logcat for LocationManager: Provider gps disabled or a network error. |
| File system case‑sensitivity on emulators | Some CI emulators use a case‑sensitive ext4 image, while most physical devices use case‑folded vfat. A hard‑coded path like /sdcard/Picture/IMG.JPG may fail on emulators. | Run the upload test on both a default Android emulator and a Google Play services image; assert that file‑access related exceptions do not appear. |
| Large burst uploads from rapid burst mode | Camera burst mode can produce 10‑20 images in under a second. If the user selects all of them, your app may attempt to concatenate them into a single multipart request, exceeding payload limits. | Use a script to generate a burst folder (e.g., for i in {1..15}; do cp base.jpg burst_$i.jpg; done) and attempt to upload the entire set via the picker’s multi‑select mode (if supported). Verify that either the app rejects excess files with a clear toast or splits them into separate requests respecting server limits. |
| Time‑zone shift during upload | A user changes device time zone while an upload is in progress; if your client timestamps the file using System.currentTimeMillis() and the server validates against a skewed clock, the request may be rejected as expired. | Change the time zone via adb shell setpersist persist.sys.timezone America/New_York mid‑upload (using a background thread to issue the command) and ensure the upload still succeeds or yields a meaningful “request expired” error rather than a cryptic 500. |
| Low‑RAM killer | On low‑end devices, the system may kill your app’s process after the picker returns but before you handle the result, leading to a null data Intent. | Use adb shell am kill com.example.app immediately after invoking startActivityForResult for the picker, then verify that onActivityResult receives resultCode = RESULT_CANCELED and that the app does not crash. |
| Network reconnection with captive portal | Public Wi‑Fi often redirects to a login page; if your app does not detect the captive portal, the upload may appear to hang. | Connect the test device to a Wi‑Fi hotspot that serves a captive portal page (you can emulate with hostapd + a simple HTTP server that returns 200 with a login HTML). Ensure that the app shows a network‑error dialog and offers a “Retry” button after the portal is cleared. |
Detecting these issues requires a combination of device‑farm testing, simulated network conditions, and intentional system state changes (time zone, permissions, kill signals). Incorporate them into your nightly test suite or into a periodic exploratory run with SUSA, which already varies device models, OS versions, and network profiles.
Accessibility and Security Considerations
Image upload touches two critical non‑functional areas: accessibility (ensuring all users can interact) and security/privacy (preventing data leakage or malicious file acceptance). Below are focused checklists you can embed in your test plans.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free