How to Test File Upload on Android (Complete Guide)
File upload is a common touchpoint in Android apps—profile pictures, document sharing, media attachments, and backup flows all rely on moving a user‑selected file from the device to a server. When thi
Why File Upload Testing Matters on Android
File upload is a common touchpoint in Android apps—profile pictures, document sharing, media attachments, and backup flows all rely on moving a user‑selected file from the device to a server. When this path fails, the impact is immediate: users cannot complete a core task, they see cryptic error messages, or worse, the app crashes or leaks data.
In production, upload failures often stem from a combination of factors that unit tests miss:
- Permission gaps – runtime denial of
READ_EXTERNAL_STORAGEorMANAGE_EXTERNAL_STORAGEon Android 13+. - Intent resolution issues – the chosen file‑picker app returns a
content://URI that the upload code cannot read. - Network interruptions – a switch from Wi‑Fi to cellular mid‑upload triggers a timeout that is not handled.
- File size limits – backend rejects files over a certain size, but the client shows a generic “upload failed” toast.
- Security oversights – uploading a file with a malicious filename can lead to path‑traversal on the server if the name is not sanitized.
Because these defects are triggered only when the app interacts with the real Android ecosystem (intents, storage providers, varying network stacks), a dedicated test strategy is essential. The following guide walks through a complete matrix, manual steps, automated scripts, and how autonomous, persona‑driven exploration can surface bugs that scripted tests never consider.
---
Building a Test Matrix for File Upload
A structured matrix helps you ensure coverage across functional, non‑functional, and security dimensions. Below is a comprehensive table that you can adapt to your app’s specific upload entry points (e.g., profile avatar, chat attachment, document upload).
| Test Category | Sub‑test | Objective | Pass Criteria | Notes |
|---|---|---|---|---|
| Happy Path | Select image from gallery | Verify UI flows to picker, returns correct URI, upload succeeds | File appears on server with correct metadata, UI shows success toast | Use a known‑good JPEG < 5 MB |
| Select PDF from Downloads | Same as above for document type | Server receives PDF, content‑type matches | ||
| Capture photo via camera intent | Confirm camera flow works, file saved to temporary location | Upload succeeds, EXIF data preserved | ||
| Validation & Error Handling | No file selected (cancel picker) | App should not attempt upload, show appropriate message | No network request, toast “No file selected” | |
| Unsupported MIME type (e.g., .exe) | App should reject before upload | Validation error shown, no upload request | ||
| Zero‑byte file | App should detect empty file | Error shown, no upload | ||
| File exceeds server limit (e.g., 20 MB limit, 25 MB file) | Client should either block or show server‑side error | App shows size‑limit message, does not waste bandwidth | ||
| Permission Scenarios | Permission denied at runtime | App should request permission, handle denial gracefully | Permission rationale shown, upload button disabled until granted | Test on Android 13 where MANAGE_EXTERNAL_STORAGE is restricted |
| Permission granted after denial | After granting, upload should work | Same as happy path | ||
| Network Conditions | Wi‑Fi → LTE handoff mid‑upload | Upload should retry or fail with clear message | App shows retry option or error after timeout | Use tc or network emulator |
| Airplane mode enabled before upload | No network request, immediate offline error | Toast “No internet connection” | ||
| High latency (300 ms+) with low bandwidth | Upload takes longer but eventually finishes or times out per config | App respects timeout setting, shows progress | ||
| Concurrency & Stress | Multiple simultaneous uploads (e.g., batch select) | All files queued, each tracked individually | Each file gets own progress indicator, no crashes | |
| Upload while app in background | Service or WorkManager continues upload | Upload completes, notification updates | Verify no foreground service misuse | |
| Accessibility | TalkBack navigation to upload button | Button is focusable, label announces purpose | Announces “Upload photo, button” | |
| Color contrast on upload icon | Meets WCAG AA (4.5:1) | Contrast ratio verified with tools | ||
| Switch control activation | Upload can be triggered via external switch | No extra steps needed | ||
| Security & Privacy | File name with path traversal chars (../../evil.exe) | Server sanitizes name; client does not crash | Upload succeeds, stored under safe name | |
| Embedded metadata (EXIF GPS) | App strips or warns about sensitive data | No location leaked unless user consents | ||
| Virus‑like content (EICAR test file) | Backend may reject; client should not execute | No crash, appropriate server response | ||
| Performance & Battery | Upload of 50 MB video over Wi‑Fi | Measure average upload speed, battery drain | Within expected thresholds (< 2 Mbps drain) | Use adb shell dumpsys batterystats |
| Repeated uploads in loop (10×) | No memory leak, no excessive wake locks | Stable memory, wake‑lock count returns to baseline |
You can expand or shrink this matrix based on risk. The key is to treat each row as a test case that can be executed manually, automated, or verified by an autonomous explorer.
---
Manual Testing Approach: Step‑by‑Step
Manual testing remains valuable for exploratory checks, UI polish, and validating error messages that are hard to assert in code. Below is a repeatable workflow you can follow on a physical device or an emulator.
1. Prepare the Test Environment
| Action | Command / UI | Purpose |
|---|---|---|
| Install the app under test | adb install -r app-debug.apk | Ensure latest build |
| Grant necessary storage permissions (pre‑emptively) | adb shell pm grant com.example.app android.permission.READ_EXTERNAL_STORAGE | Avoid flaky permission prompts |
| Clear app data to start clean | adb shell pm clear com.example.app | Reset state |
| Push test files to device storage | adb push sample.jpg /sdcard/Download/ | Provide known assets |
| (Optional) Set up network shaping | adb shell tc qdisc add dev wlan0 root netem delay 120ms limit 1000 | Simulate latency |
2. Execute the Happy Path
- Launch the app and navigate to the upload screen (e.g., tap Profile → Change Photo).
- Tap the Upload button. The system file picker should appear.
- Choose Gallery → navigate to
Download/sample.jpg→ select it. - Observe:
- The picker closes and returns to the app.
- A preview of the image appears (if implemented).
- An upload progress bar or spinner shows.
- After a few seconds, a toast reads “Upload successful” (or the server confirms).
- Verify on the backend that the file arrived with correct name, size, and MIME type.
3. Inject Error Conditions
| Scenario | Steps | Expected Observation |
|---|---|---|
| Permission denied | Revoke storage permission via Settings → Apps → Your App → Permissions → Deny. Return to app and try upload. | Permission rationale dialog appears; upload button stays disabled. |
| No file selected | Tap upload, then immediately hit back/cancel in picker. | No network request; toast “No file selected”. |
| Unsupported type | Push test.exe to Downloads, select it. | App shows “Invalid file type” before any upload attempt. |
| Zero‑byte file | Create empty file: adb shell touch /sdcard/Download/empty.txt. Select it. | App shows “File is empty”. |
| Network loss | Enable Airplane mode after picker returns but before upload completes. | Upload fails, retry option appears, or error toast shown. |
| Server‑side size limit | Attempt upload of a 30 MB video when limit is 10 MB. | Client either blocks locally (shows size error) or receives HTTP 413 and displays appropriate message. |
4. Check UI Feedback and Logs
- Use
adb logcat | grep -i uploadto watch for any exceptions or warnings. - Look for
ActivityNotFoundException(missing picker handler) orSecurityException(URI access denied). - Confirm that progress indicators are updated correctly and that the UI does not freeze (no ANR).
- Verify that error messages are user‑friendly and do not expose stack traces or internal paths.
5. Accessibility Spot‑Check
- Turn on TalkBack (
Settings → Accessibility → TalkBack). - Navigate to the upload button; ensure it announces its purpose and state.
- Verify that activating the button via TalkBack double‑tap works the same as a tap.
- Check contrast using a vision‑aid app or the Android Studio Layout Inspector.
6. Clean Up
- Remove pushed test files:
adb shell rm /sdcard/Download/sample.jpg. - Revoke any temporarily granted permissions if needed.
Repeating this checklist for each upload entry point (avatar, chat, document) gives you confidence that the core flows behave as expected before you invest in automation.
---
Automated Testing with Espresso and UIAutomator
Instrumented tests run on the device or emulator and can validate both UI interactions and underlying behavior. Espresso excels at testing within your app’s UI, while UIAutomator is needed for system dialogs like the native file picker.
1. Project Setup
Add the following dependencies to your app/build.gradle:
dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:rules:1.5.0"
// Optional: MockWebServer for backend simulation
testImplementation "com.squareup.okhttp3:mockwebserver:4.12.0"
}
Create a test class under src/androidTest/java/com/example/app/UploadTest.kt.
2. Helper to Grant Permissions
@Before
fun grantPermissions() {
// Grant storage permission for API 23+
val context = ApplicationProvider.getApplicationContext()
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED) {
GrantPermissionRule
.grant(Manifest.permission.READ_EXTERNAL_STORAGE)
.apply { }
}
}
3. Espresso Test for Happy Path (Internal Picker)
If your app uses an internal file picker (e.g., a custom RecyclerView), you can test entirely with Espresso:
@Test
fun uploadImage_success() {
// Launch the activity that contains the upload button
val activityRule = ActivityScenarioRule(MainActivity::class.java)
activityRule.scenario.onActivity { /* no extra setup needed */ }
// Click the upload button
onView(withId(R.id.btn_upload)).perform(click())
// Verify that the custom picker opens (e.g., a RecyclerView with id rv_picker)
onView(withId(R.id.rv_picker)).check(matches(isDisplayed()))
// Choose the first item (assume it's a known test image)
onView(withId(R.id.rv_picker))
.perform(RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))
// Verify progress bar appears
onView(withId(R.id.progress_upload)).check(matches(isDisplayed()))
// Wait for upload to finish – we mock the backend with MockWebServer
// Assume the app makes a POST to /upload and expects 200 OK
// Here we just idle until a specific view appears (e.g., success toast)
onView(withText(R.string.upload_success))
.withEffectiveVisibility(Visibility.VISIBLE)
.check(matches(isDisplayed()))
}
4. UIAutomator for System File Picker
When the app invokes Intent.ACTION_GET_CONTENT or Intent.ACTION_OPEN_DOCUMENT, the system picker appears. UIAutomator can interact with it:
@Test
fun uploadViaSystemPicker_success() {
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// Launch the upload flow
onView(withId(R.id.btn_upload)).perform(click())
// Wait for the system picker to appear (title contains “Documents”)
val pickerTitle = device.findObject(
UiSelector().textContains("Documents")
)
assertTrue(pickerTitle.waitForExists(5000))
// Navigate to Download folder
val downloadFolder = device.findObject(
UiSelector().text("Download")
)
downloadFolder.clickAndWaitForNewWindow()
// Select the sample image (assume file name known)
val sampleImg = device.findObject(
UiSelector().textMatches("sample\\.jpg")
)
sampleImg.clickAndWaitForNewWindow()
// Back in app – verify progress
val progress = device.findObject(
UiSelector().resourceId("com.example.app:id/progress_upload")
)
assertTrue(progress.waitForExists(10000))
// Verify success toast
val toast = device.findObject(
UiSelector().className("android.widget.Toast")
)
assertTrue(toast.waitForExists(5000))
assertTrue(toast.getText().contains("success"))
}
5. Handling Runtime Permissions in Tests
If the app requests permission at runtime, you can pre‑grant it using grantPermissionRule from androidx.test.ext.junit.rules:
@get:Rule
val permissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
)
6. Verifying Upload with MockWebServer
To avoid depending on a real backend, spin up a MockWebServer in @BeforeClass:
private lateinit var mockWebServer: MockWebServer
@BeforeClass
@JvmStatic
fun setUpServer() {
mockWebServer = MockWebServer()
mockWebServer.start()
// Enqueue a 200 response with JSON indicating success
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody("{\"status\":\"ok\"}")
)
// Optionally set the server URL in your app via manifest meta-data or DI
}
In your test, assert that the server received a request with the correct Content-Type and body (you can inspect mockWebServer.takeRequest()).
7. Running the Tests
Execute via Gradle:
./gradlew connectedAndroidTest # runs all instrumented tests
Or target a specific class:
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.example.app.UploadTest
Automated tests give you repeatable regression guards for the happy path, basic error handling, and permission flows. However, they cannot easily simulate variable network conditions, background execution nuances, or the myriad of third‑party file picker apps that users may have installed. That’s where manual exploration and autonomous testing add value.
---
Automated Testing with Appium (Cross‑platform)
Appium lets you drive the Android UI from a test script written in Java, JavaScript, Python, etc., and works equally well for native apps, hybrid WebViews, and pure web apps accessed via Chrome.
1. Desired Capabilities
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability("platformName", "Android");
caps.setCapability("deviceName", "Pixel_4_API_33");
caps.setCapability("appPackage", "com.example.app");
caps.setCapability("appActivity", ".MainActivity");
caps.setCapability("automationName", "UiAutomator2");
caps.setCapability("grantPermissions", true); // auto‑grant dangerous perms
AndroidDriver<MobileElement> driver = new AndroidDriver<>(
new URL("http://localhost:4723/wd/hub"), caps);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
2. Triggering the Upload Flow
// Click the upload button
driver.findElement(By.id("btn_upload")).click();
// Wait for the system picker to appear (context NATIVE_APP)
new WebDriverWait(driver, 15)
.until(ExpectedConditions.elementToBeClickable(By.xpath("//android.widget.TextView[@text='Documents']")));
// Navigate to Downloads
driver.findElement(By.xpath("//android.widget.TextView[@text='Download']")).click();
// Select the file
driver.findElement(By.xpath("//android.widget.TextView[@text='sample.jpg']")).click();
3. Handling the Return to App
After picking a file, the system returns control to your app. You can wait for a UI element unique to the upload screen (e.g., a progress bar):
new WebDriverWait(driver, 20)
.until(ExpectedConditions.visibilityOfElementLocated(By.id("progress_upload")));
4. Verifying Upload via Network Inspection
Appium can retrieve network logs if you enable Chromedriver logging for WebView or use the adb logcat approach:
LogEntries logEntries = driver.manage().logs().get("performance");
for (LogEntry entry : logEntries) {
Json json = Json.parse(entry.getMessage());
// Look for a request to your upload endpoint and check status
}
Alternatively, use a proxy like mitmproxy to capture requests and assert that a multipart/form‑data POST with the correct file part was sent.
5. Testing Error Scenarios
- Permission denial: Revoke permission via
adb shell pm revoke com.example.app android.permission.READ_EXTERNAL_STORAGEbefore starting the test, then assert that a permission dialog appears (driver.findElement(By.text("Allow access to photos, media, and files?"))). - Network loss: Use
adb shell svc wifi disablemid‑test, then verify that an error toast appears and the app offers a retry. - Large file: Push a 50 MB video to the device, select it, and assert that either the client blocks it (shows size error) or the server returns 413 and the app displays the message.
6. Sample JavaScript (WebDriverIO) Test
describe('File upload flow', () => {
it('should upload a picture from gallery', async () => {
await $('~btn_upload').click(); // accessibility id
await driver.waitUntil(
async => (await $('android=new UiSelector().textContains("Documents")')).isExisting(),
{ timeout: 15000, interval: 500 }
);
await $('android=new UiSelector().text("Download")').click();
await $('android=new UiSelector().text("sample.jpg")').click();
await driver.waitUntil(
async => (await $('~progress_upload')).isExisting(),
{ timeout: 20000, interval: 500 }
);
const toast = await $('android.widget.Toast');
await expect(toast).toHaveTextContaining('success');
});
});
Appium shines when you need to test across multiple device configurations or when your app includes a WebView where the file input is an HTML . In that case, you can bypass the native picker entirely by using driver.setFileDetector(new LocalFileDetector()) and then driver.findElement(By.css("input[type='file']")).sendKeys("/path/to/sample.jpg");.
---
Leveraging SUSA for Persona‑Driven Exploration
SUSA (SUSATest) is an autonomous QA agent that explores an app without pre‑written scripts. It injects a variety of user personas—each with distinct behavior patterns—into the app and monitors for crashes, ANRs, accessibility violations, security issues, and UX friction. When applied to file upload flows, SUSA can surface problems that scripted tests often miss because it does not follow a predetermined path; it tries combinations that real users might attempt out of curiosity, impatience, or confusion.
1. How SUSA Approaches an Upload Screen
When SUSA launches your app, it builds a state model of screens, buttons, and intents. For an upload screen it will:
- Tap the upload button repeatedly (simulating an impatient user).
- Attempt to open the file picker, then back out without selecting a file (curious user).
- Try to pick files from unexpected locations (e.g., the root of
/sdcard, a hidden folder). - Rotate the device, change font size, or enable TalkBack while the picker is open (accessibility persona).
- Simulate a loss of network mid‑picker (adversarial persona).
- Attempt to upload files with unusual names (
../../evil.exe, files with Unicode control characters).
Each action is logged, and SUSA checks for:
- Crashes or ANRs (via tombstone logs).
- Dead buttons (UI elements that become unresponsive after a sequence).
- WCAG violations (missing labels, insufficient contrast).
- Security signals (file‑picker returning a
content://URI that the app tries to open as aFile). - UX friction (toast messages that are vague, progress bars that never complete).
2. Running SUSA via CLI
pip install susatest-agent
susatest run \
--apk path/to/app-debug.apk \
--personas curious impatient novice accessibility \
--target upload \
--output-dir ./susa-reports
The --target upload flag tells SUSA to prioritize screens that contain an upload button or an intent filter for ACTION_GET_CONTENT. You can omit the flag to let it explore the whole app; it will still hit upload flows as part of its exploration.
3. Cross‑Session Learning
Susa stores a knowledge base of visited UI states and dead ends. On a subsequent run it:
- Skips already‑verified happy paths to focus on new combinations.
- Prioritizes states that previously caused exceptions or long load times.
- Adapts its persona weights based on what yielded the most interesting results in earlier sessions (e.g., if the “impatient” persona repeatedly triggered a race condition, future runs will allocate more taps from that persona).
This learning loop means that over time Susa becomes better at finding edge‑case bugs that are rare but high‑impact, such as a crash that only occurs when a user picks a file, rotates the screen, and then quickly cancels the picker.
4. What SUSA Finds That Scripts Miss
| Issue Type | Why Scripts May Miss It | How Susa Catches It |
|---|---|---|
| Race condition between picker cancellation and UI state reset | Scripts usually follow a linear sequence; they rarely interleave cancel actions with rapid UI changes. | The impatient persona taps cancel while the picker is still animating, exposing a null‑reference. |
| File‑provider URI permission leak on Android 10+ | Automated tests often grant broad storage permission, masking the need to take persistable permission. | Susa’s novice persona does not grant storage permission, leading to a SecurityException when trying to open the URI. |
| TalkBack label missing on dynamically generated preview thumbnails | Accessibility tests may only check static layout XML. | The accessibility persona navigates via TalkBack and reports “unlabeled button” on the preview image. |
| Server‑side validation bypass via filename with null bytes | Unit tests may sanitize strings but not test raw byte injection. | The adversarial persona sends a file whose name includes \0; the app crashes when passing the name to a native library. |
| Battery drain from a WakeLock held after upload completion | Performance tests rarely run long enough to observe lingering wakelocks. | Susa’s power‑user persona repeats uploads ten times and checks dumpsys power for stale wakelocks. |
Because SUSA treats the app as a black box and explores it with varied, semi‑randomized inputs, it complements both manual exploratory testing and automated regression suites.
---
Edge Cases That Surface Only in Production
Even with thorough lab testing, certain conditions only appear when the app runs in the wild. Below is a table of production‑only edge cases, the symptoms they cause, and the techniques you can use to detect or mitigate them.
| Production Edge Case | Typical Symptom | Detection / Mitigation Strategy |
|---|---|---|
| Variable network handoff (Wi‑Fi ↔ LTE) | Upload stalls, then fails after a long timeout; user sees stale spinner. | Use NetworkCallback to listen for CAPABILITY_NOT_METERED changes; implement exponential backoff and retry with cancelable UploadService. |
| Background execution limits (Android 9+) | Upload stops when app goes to background; user thinks it completed. | Use WorkManager with setExpedited(true) for urgent uploads, or a foreground service with a persistent notification. |
| Scoped storage & MediaStore access (Android 10+) | FileNotFoundException when trying to read a content:// URI returned by the picker. | Always open URIs via ContentResolver.openInputStream(uri); never assume a file path. |
| File provider URI permission not persisted | Subsequent attempts to re‑upload the same file fail with PermissionDenial. | Call takePersistableUriPermission(uri, flag) after picking; store the permission grant across app restarts. |
| Right‑to‑left (RTL) layout mirroring | Upload icon appears misaligned; talkback reads wrong label. | Test with adb shell setprop persist.sys.layout_direction RTL and verify layout in layout inspector. |
| Locale‑specific file name encoding | File names with non‑Latin characters appear garbled on server. | Normalize file names to UTF‑8, validate on server, and optionally transliterate for storage. |
| Carrier‑level traffic shaping (e.g., LTE throttling after certain data usage) | Upload speed drops dramatically after a few MB, causing timeout. | Detect throughput via ConnectivityManager.getNetworkCapabilities().getLinkDownstreamBandwidthKbps(); adjust chunk size or notify user. |
| Battery optimisation whitelist removal | System kills upload service during Doze mode. | Request REQUEST_IGNORE_BATTERY_OPTIMIZATIONS and guide user to whitelist the app; use setAndAllowWhileIdle() for alarms if needed. |
| Concurrent uploads from multiple apps (e.g., two chat apps trying to use the same file picker) | Picker returns ActivityNotFoundException or wrong MIME type. | Use Intent.createChooser with explicit MIME filters; handle ActivityNotFoundException gracefully. |
Unexpected file picker apps (e.g., a file manager that returns a content:// URI with no openable column) | App crashes when querying DISPLAY_NAME. | Query the URI for OpenableColumns.DISPLAY_NAME and OpenableColumns.SIZE; fall back to displaying a generic name if unavailable. |
| SD card ejection mid‑upload | IOException: No such file or directory; app may leak a partial upload. | Monitor MediaScannerConnection for MEDIA_EJECT intent; abort ongoing uploads and inform user. |
| Thermal throttling causing CPU slowdown | Upload takes much longer, leading to user-perceived lag. | Use ThermalManager.getCurrentTemperature() to adjust thread priority or show a warning when temperature exceeds threshold. |
To catch many of these issues in a lab, you can:
- Use the Android Emulator’s Extended Controls → Cellular to simulate network type changes and latency.
- Enable Developer options → Don’t keep activities to test state restoration after picker returns.
- Use
adb shell cmd appops setto revoke permissions at runtime.READ_EXTERNAL_STORAGE ignore - Leverage Firebase Test Lab or Google Play’s pre‑launch report to run on a matrix of real devices with varied locales and hardware configurations.
---
Checklist for Shipping File Upload Features
Before you mark an upload feature as “ready for release,” run through this concise checklist. It consolidates the most critical items from the matrix, manual steps, and automated guards.
| Area | Item | How to Verify |
|---|---|---|
| Functional | Happy path works for image, PDF, camera capture | Manual happy‑path test + automated Espresso/UIAutomator test |
| Cancellation does not trigger upload | Verify no network request in logcat/MockWebServer | |
| Unsupported MIME type blocked before upload | Show validation toast, no network call | |
| Zero‑byte file rejected | Show “File is empty” message | |
| File size limit enforced locally or with appropriate server error | Show size‑limit toast; avoid wasted upload | |
| Permissions | Runtime permission request shown when denied | Revoke permission, attempt upload, see rationale dialog |
| Permission granted after denial enables upload | Grant via Settings, retry upload | |
| Persistable URI permission taken for content URIs | Check contentResolver.takePersistableUriPermission call | |
| Network | Upload survives Wi‑Fi → LTE handoff | Use network shaping tool, assert retry or proper error |
| Offline state yields clear error | Enable Airplane mode, verify toast | |
| High latency does not crash app | Add 300 ms delay, ensure timeout handling | |
| Concurrency | Multiple files queued, each tracked | Select several images, verify independent progress bars |
| Background upload completes | Press home, check notification, verify server receipt | |
| Accessibility | Upload button TalkBack label correct | Enable TalkBack, navigate, hear label |
| Sufficient color contrast | Use Accessibility Scanner or Android Studio inspector | |
| Switch control activation works | Pair external switch, trigger upload | |
| Security | File name with path traversal sanitized |
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