All articles

Technical

How to Handle File Downloads inside an Android TWA

October 26, 2026 · 6 min read

When developers transition a Progressive Web App (PWA) to the Google Play Store using a Trusted Web Activity (TWA), they expect web APIs to behave exactly as they do in a standard mobile browser. While this is true for layout engines, styling, and caching, file execution and storage access present unique architectural hurdles. A common point of failure is file downloads.

In a standard web browser like Chrome, clicking a download link triggers an internal system event that passes the request to the Android Download Manager. Inside a TWA, however, this bridge can fail silently. Users click a download button, but no file is saved, no progress bar appears, and no error message is displayed. Understanding why this happens and how to work around it is crucial for delivering a robust native-feeling application.

The Core Reason Downloads Fail in a TWA

A TWA runs within the context of a custom-branded Chrome instance. However, unlike the standard Chrome browser, the TWA wrapper does not always inherit automatic write permissions to the user filesystem, nor does it have an active user interface for managing system downloads unless configured correctly. When a web application initiates a download, the browser engine attempts to hand off the URL payload to the Android operating system. If this handoff is blocked by permission scope restrictions, lack of directory access, or invalid payload structures, the system rejects the request without throwing a visible console exception.

This failure occurs most frequently with dynamically generated client-side files, such as those created using Blob object URLs, or files secured behind session authentication states that require cookie passing. To ensure files reach the storage folder of the user, developers must use specific transfer strategies.

Standard Link Downloads and Server Headers

The most reliable method for downloading files in a TWA is to rely on direct HTTP URLs served with explicit headers. When the server delivers a file, it should include specific HTTP response headers to force Chrome to bypass viewport rendering and send the data directly to the native download handler.

The key header to configure on your hosting server is the Content-Disposition header. By setting this to attachment, you instruct the web engine that the resource is not meant to be viewed in-app but rather stored locally.

Header NameValue ExamplePurpose
Content-Dispositionattachment; filename="report.pdf"Forces the browser to trigger a download prompt and defines the filename.
Content-Typeapplication/octet-streamTells the browser the file is binary data, preventing inline rendering.
Cache-Controlno-cachePrevents the browser from caching corrupted partial downloads.

When the TWA engine detects an incoming HTTP response with these headers, it triggers the Android Download Manager, which handles the file transfer on behalf of the application, showing a system-level progress bar in the notification drawer.

The Problem with Blob and Data URLs

Many modern single-page applications generate files dynamically in the browser using the JavaScript Blob API or by producing Base64-encoded Data URLs. While code like URL.createObjectURL(blob) works seamlessly in a desktop browser, it often fails inside a TWA. This is because the generated string is a temporary, in-memory reference that only exists within the running JavaScript context. When the TWA attempts to share this local memory reference with the external Android Download Manager, the system fails to access it because the manager resides in a separate process space.

If your application must handle dynamic client-side generation, you should avoid passing raw Blob URLs to anchor tags. Instead, convert the data back to a standard server-side endpoint if possible, or trigger a helper function that redirects the user to an external system handler using standard navigation pathways.

Routing Downloads Through the External Browser

If you have legacy files, complex client-side generation, or protected downloads that cannot easily trigger the native TWA download process, a reliable alternative is to offload the download action to the primary system browser. By opening the download link in a standard browser window, you leverage the full permission set and download interface of the standalone Chrome or default browser application.

To force a link to open outside the TWA container, the URL target must fall outside the digital asset link scope defined in your app manifest, or you must explicitly target a new window. Using target-blank on a domain that is not declared inside your asset links configuration file will tell the Android operating system to launch the default external browser to handle the request.

For example, you can host your download files on a dedicated subdomain, such as download.yourdomain.com, and deliberately exclude this subdomain from your Digital Asset Links file. When the user initiates a download, the app detects that the destination is outside its verified scope and safely opens it in the system browser, where the download completes instantly.

Verifying Android Permissions

Depending on the targeted Android API level, your application may require explicit storage permissions declared in the Android Manifest. For modern Android versions (Android 10 and higher), scoped storage rules mean that applications do not require broad filesystem access to save files into the public Downloads directory. However, for compatibility with legacy devices, ensuring your package generator includes appropriate read and write permission flags inside the underlying template is key to preventing system blocks.

By structuring your web application to serve attachments with correct HTTP headers and gracefully routing complex exports outside the TWA wrapper, you can guarantee a seamless file utility experience that matches native app behavior.

Ready to ship your Android app?

Paste your PWA URL, get a signed APK and a Google Play ready AAB in minutes.

Build my app