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:

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:

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

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

Questions


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.