Search code examples
androidtimeoutsamsung-mobilefilesaver.jssamsung-internet

File Download Prompt not Showing in Samsung Internet Browser App on a Samsung Galaxy Phone


Context and Problem Statement
We are currently developing a single-page-application with an API backend. The API provides files to the single-page-application which are generated on-the-fly on request on the server. Some files take very little time to be generated, others take longer (> 5 seconds). Now we face the issue, that downloading those files which take more than 5 seconds to be generated, is not working in the "Samsung Internet" browser on a Samsung Galaxy phone. On such devices, the prompt to save the file is not shown. The file is indeed being downloaded, but the prompt to save the file on the device is just not shown. Like this, the users cannot open or store the file on these devices.

The download works in all desktop browsers which we tested and also in the Chrome browser on a Samsung Galaxy phone. It does just not work in the "Samsung Internet" browser on a Samsung Galaxy phone.

Version of my Samsung Galaxy phone and "Samsung Internet" browser:

  • Model: Samsung Galaxy S22+
  • One UI version: 5.1
  • Android version: 13
  • "Samsung Internet" app version: 21.0.0.41 (this is the current latest version of the "Samsung Internet" app)

How to Reproduce
We extracted the relevant code from our project and created a simple webserver and client to demonstrate the issue. You can reproduce the issue by following these steps:

  • Clone the repository at https://github.com/tobias-graf-p/file-download-issue
  • Navigate into the server folder and start the server on your computer. Please consult the README file inside the server folder for detailed instructions.
  • Navigate into the client-js folder and serve the client from your computer. Please consult the README file inside the client-js folder for detailed instructions.
  • Now open the client on your computer by navigating to http://localhost:8081. You should be able to download both files. File 1 is being served immediately and file 2 is being served with a delay of 6 seconds.
  • Now use a Samsung Galaxy phone which is connected to the same network as your computer. Use the "Samsung Internet" browser to navigate to http://your-local-ip-address:8081 (replace your-local-ip-address with the actual local ip address of your computer). Here you should be able to download file 1. But if you try to download file 2, nothing happens (after 6 seconds). The "Samsung Internet" browser does not show a prompt to the user to open or store file 2 (as it does for file 1).

How to Debug
You can debug the client running on your phone using the following steps:

  • Enable USB debugging in the Developer options on your Samsung Galaxy phone:
    • Go to Settings > About phone > Software information
    • Tap 7 times on Build number
    • Go to Settings > Developer options
    • Enable USB debugging
  • Inspect the client with Google Chrome:
    • Connect the phone to your computer via a USB cable.
    • Go to chrome://inspect/ in the Google Chrome browser on your computer.
    • Acknowledge the permission request on the phone.
    • Wait for the device to appear in the Chrome browser on your computer.
    • Navigate to http://your-local-ip-address:8081 (replace your-local-ip-address with the actual local ip address of your computer) in the "Samsung Internet" browser on your phone.
    • Wait for the page to appear in the Chrome browser on your computer
    • Click inspect

This opens a new Chrome browser windows which shows you the client running on your phone. Now you can open the Chrome developer tools (F12) and analyze the network traffic and the console output of the client. You will see that file 2 is indeed being sent to the client, but that no prompt is shown on the phone for file 2.

Code Snippets
All the relevant code to reproduce the issue is available in this Github repository: https://github.com/tobias-graf-p/file-download-issue

As a reference, I post the relevant pieces of code here:

Code to serve the files (server), full file

function serveFile(res, filePath, fileName, contentType) {
  const contentDisposition = `attachment; filename="${encodeURIComponent(fileName)}"`;
  res.setHeader('Content-Disposition', contentDisposition);
  res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
  res.setHeader('Content-Type', contentType);
  res.setHeader('Access-Control-Allow-Origin', '*');
  const fileStream = fs.createReadStream(filePath);
  fileStream.pipe(res);
}

const server = http.createServer((req, res) => {
  if (req.url === '/file1') {
    const filePath = path.join(__dirname, 'file1.txt');
    serveFile(res, filePath, 'file1.txt', 'text/plain');
  } else if (req.url === '/file2') {
    setTimeout(() => {
      const filePath = path.join(__dirname, 'file2.txt');
      serveFile(res, filePath, 'file2.txt', 'text/plain');
    }, 6000);
  } else {
    res.statusCode = 404;
    res.end('Not found');
  }
});

Code to download the files (client), full file

async function downloadFile(endpoint) {
  console.log('downloadFile()');
  console.log('endpoint', endpoint);

  const response = await fetch(endpoint);
  console.log('response', response);

  const blob = await response.blob();
  console.log('blob', blob);

  const url = URL.createObjectURL(blob);
  console.log('url', url);

  const contentDispositionHeader = response.headers.get('Content-Disposition');
  const fileName = getFileName(contentDispositionHeader);
  console.log('fileName', fileName);

  const downloadLinkTag = document.createElement('a');
  downloadLinkTag.href = url;
  downloadLinkTag.download = fileName;

  console.log('before click');
  downloadLinkTag.click();
  console.log('after click');

  setTimeout(() => URL.revokeObjectURL(url), 0);
}

function getFileName(contentDispositionHeader) {
  let fileName = contentDispositionHeader
    .split(';')[1]
    .split('=')[1];
  if (fileName.startsWith('"')) {
    fileName = fileName.substring(1, fileName.length - 1);
  }
  if (fileName.endsWith('"')) {
    fileName = fileName.substring(0, fileName.length - 2);
  }
  return decodeURI(fileName);
}

There is a second client (simple Angular app) available in the Github repository with which you also can reproduce the issue as well. This client contains even three different approaches to download the file (using an a-tag with object url, using FileSaver.js and using a FileReader), which all fail the same way (no prompt for the file with the delay).

Code of the three approaches, full file

private downloadFile(apiUrl: string): void {
  this.http
    .get(apiUrl, { responseType: 'blob', observe: 'response' })
    .subscribe(response => {
      const fileName = this.getFileNameFromHeaders(response.headers);
      console.log('fileName', fileName);

      //
      // Approach #1: a-tag with object url
      //

      console.log('approach #1');
      const data = response.body;
      if (!data) {
        console.log('no data');
        return;
      }
      console.log('data', data);
      const url = URL.createObjectURL(data);
      console.log('url', url);
      const link = document.createElement('a');
      link.href = url;
      link.download = fileName;
      console.log('before click');
      link.click();
      console.log('after click');
      setTimeout(() => URL.revokeObjectURL(url), 0);

      //
      // Approach #2: FileSaver.js
      //

      console.log('approach #2');
      const blob = new Blob([response.body as Blob], {type: 'text/plain'});
      console.log('blob', blob);
      console.log('before saveAs');
      saveAs(blob, fileName);
      console.log('after saveAs');

      //
      // Approach #3: FileReader
      //

      console.log('approach #3');
      const reader = new FileReader();
      reader.onloadend = function(e) {
        console.log('reader.result', reader.result);
        const link = document.createElement('a');
        document.body.appendChild(link);
        link.href = reader.result as string;
        link.download = fileName;
        const clickEvent = new MouseEvent('click');
        console.log('before dispatch click event');
        link.dispatchEvent(clickEvent);
        console.log('after dispatch click event');
        setTimeout(()=> {
          document.body.removeChild(link);
        }, 0)
      }
      console.log('response.body', response.body);
      console.log('before readAsDataURL');
      reader.readAsDataURL(response.body as Blob);
      console.log('after readAsDataURL');
    });
}

private getFileNameFromHeaders(headers: HttpHeaders): string {
  const contentDisposition = headers.get('Content-Disposition');
  if (!contentDisposition) {
    return 'unknown.txt';
  }
  let fileName = contentDisposition
    .split(';')[1]
    .split('=')[1];
  if (fileName.startsWith('"')) {
    fileName = fileName.substring(1, fileName.length - 1);
  }
  if (fileName.endsWith('"')) {
    fileName = fileName.substring(0, fileName.length - 2);
  }
  return decodeURI(fileName);
}

Additional Information

  • We found that the issue occurs not only if the server needs some time (> 5 seconds) to respond, but also if we add a delay of more than 5 seconds at the client side before simulating the click on the anchor tag. It seems, that the "Samsung Internet" browser applies a timeout of 5 seconds after the user clicks the download button (what it probably shouldn't).
  • Moreover we found that removing the download attribute from the a tag makes the "Samsung Internet" browser actually show the downloaded file. But it shows the content of the downloaded file in the browser window then, which is not an option for us. (We would like to prompt the users to open or store the file.) We can trick the "Samsung Internat" browser to show the prompt again with using const data = new Blob([data as Blob], {type: 'application/octet-stream'}); instead of const data = response.body; but with this approach we loose the file name. (The browser now asks the user to store the file with the blob guid as the file name, which is not an option for us neither.)

Questions

  • Are we doing something wrong? Is there a (alternative) way to make the download prompt show on Samsung Galaxy phones?
  • Could this be a bug in the "Samsung Internet" browser?

Solution

  • Samsung responded to my question in the Samsung Developers Forums and confirmed, that this behavior is indeed by design. A user can disable this behavior by disabling the option "Browsing privacy dashboard" > "Block automatic downloads" in the settings of the Samsung Internet browser.

    Answer in the Samsung Developers Forums:
    https://forum.developer.samsung.com/t/file-download-prompt-not-showing-in-samsung-internet-browser-app/25740

    Answer from "Samsung Members":
    This is a security patch to prevent the infinite automatic downloads. Chromium automatic downloads implementation is different from ours. Chromium stores automatic download permission on url basis.In case of Samsung Internet we have a common button in Browsing privacy dashboard. When the developer delays the download by 5 or more seconds, the boolean value has_gesture is false (because function HasTransientUserActivation() reset its value after a specific timeout) hence we block the download immediately. To overcome this we request the developer to either don't delay the downlead by 5 or more seconds or disable the block automatic download button in Browser Privacy Dashboard.