Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@
*/
package com.owncloud.android.ui.activity;

import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.view.View;
import android.webkit.JavascriptInterface;
Expand All @@ -37,6 +34,7 @@
import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.MimeTypeUtil;
import com.owncloud.android.utils.RichDocumentDownloader;
import com.owncloud.android.utils.WebViewUtil;

import java.util.ArrayList;
Expand Down Expand Up @@ -285,23 +283,13 @@ protected void setThumbnailView(final User user) {
}
}

protected void downloadFile(Uri url, String fileName) {
DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);

if (downloadmanager == null) {
DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_download));
return;
}

DownloadManager.Request request = new DownloadManager.Request(url);
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);

// change the name file and your current activity.
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);


downloadmanager.enqueue(request);
protected void downloadFile(Uri uri, String filename) {
// downloadAs is invoked from the WebView JavaScript bridge thread, but WebView methods
// (getSettings) must run on the main thread, so read the user agent there.
runOnUiThread(() -> {
String userAgent = getWebView().getSettings().getUserAgentString();
new RichDocumentDownloader(this).download(uri, filename, userAgent);
});
}

public void setLoadingSnackbar(Snackbar loadingSnackbar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,8 @@ class RichDocumentsEditorWebView : EditorWebView() {
val url = result.url.toUri()
when (result.format) {
PRINT -> printFile(url)

SLIDESHOW -> showSlideShow(url)

else -> {
downloadFile(url, result.filename)
}
else -> downloadFile(url, result.filename)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

package com.owncloud.android.ui.model

data class DownloadAs(val format: String, val filename: String, val url: String)
data class DownloadAs(val format: String, val filename: String?, val url: String)
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ object RichDocumentDownloadAsParser {
val format = obj[FORMAT]?.jsonPrimitive?.contentOrNull
val name = obj[NAME]?.jsonPrimitive?.contentOrNull
if (format == null || url == null) return null
return DownloadAs(format = format, filename = name ?: "", url = url)
return DownloadAs(format = format, filename = name, url = url)
}

private fun tryParseV1(obj: JsonObject, url: String?): DownloadAs? {
val type = obj[TYPE]?.jsonPrimitive?.contentOrNull
val filename = obj[FILENAME]?.jsonPrimitive?.contentOrNull
if (type == null || url == null) return null
return DownloadAs(format = type, filename = filename ?: "", url = url)
return DownloadAs(format = type, filename = filename, url = url)
}
}
139 changes: 139 additions & 0 deletions app/src/main/java/com/owncloud/android/utils/RichDocumentDownloader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.owncloud.android.utils

import android.app.DownloadManager
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.webkit.CookieManager
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.owncloud.android.R
import com.owncloud.android.lib.common.utils.Log_OC
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import java.io.File
import java.net.URLDecoder
import java.util.UUID

class RichDocumentDownloader(private val activity: AppCompatActivity) {
private val client = OkHttpClient()

fun download(uri: Uri, filename: String?, userAgent: String?) {
activity.lifecycleScope.launch(Dispatchers.IO) {
runCatching {
// if filename not provided get filename from header
if (filename == null) {
val url = uri.toString()
downloadWithOkHttp(url, userAgent)
} else {
downloadWithDownloadManager(uri, filename)
}
}
.onFailure { Log_OC.e(TAG, "download failed: $it") }
.getOrDefault(false)
}
}

private suspend fun downloadWithDownloadManager(url: Uri, filename: String) = withContext(Dispatchers.Main) {
val downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager
?: return@withContext showMessage(false)

DownloadManager.Request(url).apply {
@Suppress("DEPRECATION")
allowScanningByMediaScanner()
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
}.let(downloadManager::enqueue)
}

private fun downloadWithOkHttp(url: String, userAgent: String?) {
val requestBuilder = Request.Builder().url(url).get()

CookieManager.getInstance().getCookie(url)?.let { requestBuilder.header(COOKIE_HEADER, it) }
userAgent?.let { requestBuilder.header(USER_AGENT_HEADER, it) }

client.newCall(requestBuilder.build()).execute().use { response ->
val body = response.body
if (!response.isSuccessful) {
Log_OC.e(TAG, "server returned ${response.code} for download")
showMessage(false)
return
}

val disposition = response.header(CONTENT_DISPOSITION_HEADER)
val resolvedName = resolveFileName(disposition)
val result = saveToDownloads(body, resolvedName)
showMessage(result)
}
}

private fun showMessage(success: Boolean) {
activity.runOnUiThread {
val message = if (success) {
R.string.downloader_download_succeeded_ticker
} else {
R.string.failed_to_download
}

DisplayUtils.showSnackMessage(activity, message)
}
}

private fun saveToDownloads(body: ResponseBody, filename: String): Boolean {
val mimeType = body.contentType()?.let { "${it.type}/${it.subtype}" } ?: DEFAULT_MIME_TYPE
val tempFile = File.createTempFile(TEMP_PREFIX, null, activity.cacheDir)

return try {
tempFile.outputStream().use { output -> body.byteStream().copyTo(output) }
FileExportUtils().exportFile(filename, mimeType, activity.contentResolver, null, tempFile)
true
} finally {
tempFile.delete()
}
}

private fun resolveFileName(disposition: String?): String =
extractFilenameFromDisposition(disposition) ?: randomFilename()

private fun extractFilenameFromDisposition(disposition: String?): String? {
if (disposition.isNullOrBlank()) return null
return extractExtendedFilename(disposition) ?: extractPlainFilename(disposition)
}

private fun extractExtendedFilename(disposition: String): String? {
val regex = EXTENDED_FILENAME_REGEX.toRegex(RegexOption.IGNORE_CASE)
val value = regex.find(disposition)?.groupValues?.get(1)?.trim() ?: return null
return runCatching { URLDecoder.decode(value, EXTENDED_FILENAME_ENCODER) }.getOrNull()
}

private fun extractPlainFilename(disposition: String): String? {
val regex = PLAIN_FILENAME_REGEX.toRegex(RegexOption.IGNORE_CASE)
return regex.find(disposition)?.groupValues?.get(1)?.trim()
}

private fun randomFilename(): String = "file_" + UUID.randomUUID().toString().take(RANDOM_NAME_LENGTH)

companion object {
private const val TAG = "RichDocumentDownloader"
private const val COOKIE_HEADER = "Cookie"
private const val USER_AGENT_HEADER = "User-Agent"
private const val CONTENT_DISPOSITION_HEADER = "Content-Disposition"
private const val DEFAULT_MIME_TYPE = "application/octet-stream"
private const val TEMP_PREFIX = "richdocument_download"
private const val RANDOM_NAME_LENGTH = 8
private const val EXTENDED_FILENAME_REGEX = """filename\*\s*=\s*[^']*''([^;]+)"""
private const val PLAIN_FILENAME_REGEX = """filename\s*=\s*"?([^";]+)"?"""
private const val EXTENDED_FILENAME_ENCODER = "UTF-8"
}
}
Loading