Skip to content

Commit 7b51a44

Browse files
Merge pull request #17235 from nextcloud/fix/re-download-office-files
fix(re-download): office file
2 parents b2acf70 + 6652803 commit 7b51a44

5 files changed

Lines changed: 151 additions & 28 deletions

File tree

app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
*/
88
package com.owncloud.android.ui.activity;
99

10-
import android.app.DownloadManager;
1110
import android.content.ActivityNotFoundException;
1211
import android.content.ClipData;
13-
import android.content.Context;
1412
import android.content.Intent;
1513
import android.graphics.Bitmap;
1614
import android.graphics.drawable.Drawable;
1715
import android.graphics.drawable.LayerDrawable;
1816
import android.net.Uri;
19-
import android.os.Environment;
2017
import android.os.Handler;
2118
import android.view.View;
2219
import android.webkit.JavascriptInterface;
@@ -37,6 +34,7 @@
3734
import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask;
3835
import com.owncloud.android.utils.DisplayUtils;
3936
import com.owncloud.android.utils.MimeTypeUtil;
37+
import com.owncloud.android.utils.RichDocumentDownloader;
4038
import com.owncloud.android.utils.WebViewUtil;
4139

4240
import java.util.ArrayList;
@@ -285,23 +283,13 @@ protected void setThumbnailView(final User user) {
285283
}
286284
}
287285

288-
protected void downloadFile(Uri url, String fileName) {
289-
DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
290-
291-
if (downloadmanager == null) {
292-
DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_download));
293-
return;
294-
}
295-
296-
DownloadManager.Request request = new DownloadManager.Request(url);
297-
request.allowScanningByMediaScanner();
298-
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
299-
300-
// change the name file and your current activity.
301-
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
302-
303-
304-
downloadmanager.enqueue(request);
286+
protected void downloadFile(Uri uri, String filename) {
287+
// downloadAs is invoked from the WebView JavaScript bridge thread, but WebView methods
288+
// (getSettings) must run on the main thread, so read the user agent there.
289+
runOnUiThread(() -> {
290+
String userAgent = getWebView().getSettings().getUserAgentString();
291+
new RichDocumentDownloader(this).download(uri, filename, userAgent);
292+
});
305293
}
306294

307295
public void setLoadingSnackbar(Snackbar loadingSnackbar) {

app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,8 @@ class RichDocumentsEditorWebView : EditorWebView() {
168168
val url = result.url.toUri()
169169
when (result.format) {
170170
PRINT -> printFile(url)
171-
172171
SLIDESHOW -> showSlideShow(url)
173-
174-
else -> {
175-
downloadFile(url, result.filename)
176-
}
172+
else -> downloadFile(url, result.filename)
177173
}
178174
}
179175

app/src/main/java/com/owncloud/android/ui/model/DownloadAs.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77

88
package com.owncloud.android.ui.model
99

10-
data class DownloadAs(val format: String, val filename: String, val url: String)
10+
data class DownloadAs(val format: String, val filename: String?, val url: String)

app/src/main/java/com/owncloud/android/utils/RichDocumentDownloadAsParser.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ object RichDocumentDownloadAsParser {
4747
val format = obj[FORMAT]?.jsonPrimitive?.contentOrNull
4848
val name = obj[NAME]?.jsonPrimitive?.contentOrNull
4949
if (format == null || url == null) return null
50-
return DownloadAs(format = format, filename = name ?: "", url = url)
50+
return DownloadAs(format = format, filename = name, url = url)
5151
}
5252

5353
private fun tryParseV1(obj: JsonObject, url: String?): DownloadAs? {
5454
val type = obj[TYPE]?.jsonPrimitive?.contentOrNull
5555
val filename = obj[FILENAME]?.jsonPrimitive?.contentOrNull
5656
if (type == null || url == null) return null
57-
return DownloadAs(format = type, filename = filename ?: "", url = url)
57+
return DownloadAs(format = type, filename = filename, url = url)
5858
}
5959
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.utils
9+
10+
import android.app.DownloadManager
11+
import android.content.Context
12+
import android.net.Uri
13+
import android.os.Environment
14+
import android.webkit.CookieManager
15+
import androidx.appcompat.app.AppCompatActivity
16+
import androidx.lifecycle.lifecycleScope
17+
import com.owncloud.android.R
18+
import com.owncloud.android.lib.common.utils.Log_OC
19+
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.launch
21+
import kotlinx.coroutines.withContext
22+
import okhttp3.OkHttpClient
23+
import okhttp3.Request
24+
import okhttp3.ResponseBody
25+
import java.io.File
26+
import java.net.URLDecoder
27+
import java.util.UUID
28+
29+
class RichDocumentDownloader(private val activity: AppCompatActivity) {
30+
private val client = OkHttpClient()
31+
32+
fun download(uri: Uri, filename: String?, userAgent: String?) {
33+
activity.lifecycleScope.launch(Dispatchers.IO) {
34+
runCatching {
35+
// if filename not provided get filename from header
36+
if (filename == null) {
37+
val url = uri.toString()
38+
downloadWithOkHttp(url, userAgent)
39+
} else {
40+
downloadWithDownloadManager(uri, filename)
41+
}
42+
}
43+
.onFailure { Log_OC.e(TAG, "download failed: $it") }
44+
.getOrDefault(false)
45+
}
46+
}
47+
48+
private suspend fun downloadWithDownloadManager(url: Uri, filename: String) = withContext(Dispatchers.Main) {
49+
val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager
50+
?: return@withContext showMessage(false)
51+
52+
DownloadManager.Request(url).apply {
53+
@Suppress("DEPRECATION")
54+
allowScanningByMediaScanner()
55+
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
56+
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
57+
}.let(downloadManager::enqueue)
58+
}
59+
60+
private fun downloadWithOkHttp(url: String, userAgent: String?) {
61+
val requestBuilder = Request.Builder().url(url).get()
62+
63+
CookieManager.getInstance().getCookie(url)?.let { requestBuilder.header(COOKIE_HEADER, it) }
64+
userAgent?.let { requestBuilder.header(USER_AGENT_HEADER, it) }
65+
66+
client.newCall(requestBuilder.build()).execute().use { response ->
67+
val body = response.body
68+
if (!response.isSuccessful) {
69+
Log_OC.e(TAG, "server returned ${response.code} for download")
70+
showMessage(false)
71+
return
72+
}
73+
74+
val disposition = response.header(CONTENT_DISPOSITION_HEADER)
75+
val resolvedName = resolveFileName(disposition)
76+
val result = saveToDownloads(body, resolvedName)
77+
showMessage(result)
78+
}
79+
}
80+
81+
private fun showMessage(success: Boolean) {
82+
activity.runOnUiThread {
83+
val message = if (success) {
84+
R.string.downloader_download_succeeded_ticker
85+
} else {
86+
R.string.failed_to_download
87+
}
88+
89+
DisplayUtils.showSnackMessage(activity, message)
90+
}
91+
}
92+
93+
private fun saveToDownloads(body: ResponseBody, filename: String): Boolean {
94+
val mimeType = body.contentType()?.let { "${it.type}/${it.subtype}" } ?: DEFAULT_MIME_TYPE
95+
val tempFile = File.createTempFile(TEMP_PREFIX, null, activity.cacheDir)
96+
97+
return try {
98+
tempFile.outputStream().use { output -> body.byteStream().copyTo(output) }
99+
FileExportUtils().exportFile(filename, mimeType, activity.contentResolver, null, tempFile)
100+
true
101+
} finally {
102+
tempFile.delete()
103+
}
104+
}
105+
106+
private fun resolveFileName(disposition: String?): String =
107+
extractFilenameFromDisposition(disposition) ?: randomFilename()
108+
109+
private fun extractFilenameFromDisposition(disposition: String?): String? {
110+
if (disposition.isNullOrBlank()) return null
111+
return extractExtendedFilename(disposition) ?: extractPlainFilename(disposition)
112+
}
113+
114+
private fun extractExtendedFilename(disposition: String): String? {
115+
val regex = EXTENDED_FILENAME_REGEX.toRegex(RegexOption.IGNORE_CASE)
116+
val value = regex.find(disposition)?.groupValues?.get(1)?.trim() ?: return null
117+
return runCatching { URLDecoder.decode(value, EXTENDED_FILENAME_ENCODER) }.getOrNull()
118+
}
119+
120+
private fun extractPlainFilename(disposition: String): String? {
121+
val regex = PLAIN_FILENAME_REGEX.toRegex(RegexOption.IGNORE_CASE)
122+
return regex.find(disposition)?.groupValues?.get(1)?.trim()
123+
}
124+
125+
private fun randomFilename(): String = "file_" + UUID.randomUUID().toString().take(RANDOM_NAME_LENGTH)
126+
127+
companion object {
128+
private const val TAG = "RichDocumentDownloader"
129+
private const val COOKIE_HEADER = "Cookie"
130+
private const val USER_AGENT_HEADER = "User-Agent"
131+
private const val CONTENT_DISPOSITION_HEADER = "Content-Disposition"
132+
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
133+
private const val TEMP_PREFIX = "richdocument_download"
134+
private const val RANDOM_NAME_LENGTH = 8
135+
private const val EXTENDED_FILENAME_REGEX = """filename\*\s*=\s*[^']*''([^;]+)"""
136+
private const val PLAIN_FILENAME_REGEX = """filename\s*=\s*"?([^";]+)"?"""
137+
private const val EXTENDED_FILENAME_ENCODER = "UTF-8"
138+
}
139+
}

0 commit comments

Comments
 (0)