diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 8e9088e93b..9f51d082f8 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -411 \ No newline at end of file +426 diff --git a/src/main/java/com/owncloud/android/ui/activity/RichDocumentsWebView.java b/src/main/java/com/owncloud/android/ui/activity/RichDocumentsWebView.java index af07a4cbe4..f08055305a 100644 --- a/src/main/java/com/owncloud/android/ui/activity/RichDocumentsWebView.java +++ b/src/main/java/com/owncloud/android/ui/activity/RichDocumentsWebView.java @@ -49,16 +49,20 @@ import android.widget.Toast; import com.bumptech.glide.Glide; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.client.account.CurrentAccountProvider; +import com.nextcloud.client.network.ClientFactory; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.Template; import com.owncloud.android.datamodel.ThumbnailsCacheManager; +import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.operations.RichDocumentsCreateAssetOperation; import com.owncloud.android.ui.asynctasks.LoadUrlTask; +import com.owncloud.android.ui.asynctasks.PrintAsyncTask; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.utils.DisplayUtils; +import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.glide.CustomGlideStreamLoader; @@ -66,6 +70,9 @@ import org.json.JSONException; import org.json.JSONObject; import org.parceler.Parcels; +import java.io.File; +import java.lang.ref.WeakReference; + import javax.inject.Inject; import androidx.annotation.RequiresApi; @@ -81,12 +88,14 @@ import lombok.Setter; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class RichDocumentsWebView extends ExternalSiteWebView { - private static final String TAG = RichDocumentsWebView.class.getSimpleName(); - private static final int REQUEST_REMOTE_FILE = 100; - - public static final int REQUEST_LOCAL_FILE = 101; - public static final int MINIMUM_API = Build.VERSION_CODES.LOLLIPOP; + public static final int REQUEST_LOCAL_FILE = 101; + private static final int REQUEST_REMOTE_FILE = 100; + private static final String TAG = RichDocumentsWebView.class.getSimpleName(); + private static final String URL = "URL"; + private static final String TYPE = "Type"; + private static final String PRINT = "print"; + private static final String NEW_NAME = "NewName"; private Unbinder unbinder; private OCFile file; @@ -106,6 +115,9 @@ public class RichDocumentsWebView extends ExternalSiteWebView { @Inject protected CurrentAccountProvider currentAccountProvider; + @Inject + protected ClientFactory clientFactory; + @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used >= Lollipop @Override protected void onCreate(Bundle savedInstanceState) { @@ -196,8 +208,8 @@ public class RichDocumentsWebView extends ExternalSiteWebView { if (file.isFolder()) { thumbnailView.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() || - file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), file.getMountType(), - this)); + file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), file.getMountType(), + this)); } else { if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) { // Thumbnail in cache? @@ -217,7 +229,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { try { final ThumbnailsCacheManager.ThumbnailGenerationTask task = new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView, - getStorageManager(), getAccount()); + getStorageManager(), getAccount()); if (thumbnail == null) { if (MimeTypeUtil.isVideo(file)) { @@ -230,7 +242,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { new ThumbnailsCacheManager.AsyncThumbnailDrawable(getResources(), thumbnail, task); thumbnailView.setImageDrawable(asyncDrawable); task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, - file.getRemoteId())); + file.getRemoteId())); } catch (IllegalArgumentException e) { Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage()); } @@ -242,7 +254,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { } } else { thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(), - getAccount(), this)); + getAccount(), this)); } } } @@ -310,7 +322,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { String asset = (String) result.getSingleData(); runOnUiThread(() -> webview.evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" + - file.getFileName() + "', '" + asset + "');", null)); + file.getFileName() + "', '" + asset + "');", null)); } else { runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!")); } @@ -361,6 +373,34 @@ public class RichDocumentsWebView extends ExternalSiteWebView { "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", null); } + private void printFile(Uri url) { + OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); + + if (account == null) { + DisplayUtils.showSnackMessage(webview, getString(R.string.failed_to_print)); + return; + } + + File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf"); + + new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute(); + } + + private void downloadFile(Uri url) { + DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); + + if (downloadmanager == null) { + DisplayUtils.showSnackMessage(webview, getString(R.string.failed_to_download)); + return; + } + + DownloadManager.Request request = new DownloadManager.Request(url); + request.allowScanningByMediaScanner(); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + + downloadmanager.enqueue(request); + } + private class RichDocumentsMobileInterface { @JavascriptInterface public void close() { @@ -384,27 +424,22 @@ public class RichDocumentsWebView extends ExternalSiteWebView { @JavascriptInterface public void downloadAs(String json) { - Uri downloadUrl; try { JSONObject downloadJson = new JSONObject(json); - downloadUrl = Uri.parse(downloadJson.getString("URL")); + + Uri url = Uri.parse(downloadJson.getString(URL)); + + if (downloadJson.getString(TYPE).equalsIgnoreCase(PRINT)) { + printFile(url); + } else { + downloadFile(url); + } } catch (JSONException e) { - Log_OC.e(this, "Failed to parse rename json message: " + e); + Log_OC.e(this, "Failed to parse download json message: " + e); return; } - DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); - if (downloadmanager == null) { - DisplayUtils.showSnackMessage(webview, getString(R.string.failed_to_download)); - return; - } - - DownloadManager.Request request = new DownloadManager.Request(downloadUrl); - request.allowScanningByMediaScanner(); - request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - - downloadmanager.enqueue(request); } @JavascriptInterface @@ -413,7 +448,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { // need to change filename for sharing try { JSONObject renameJson = new JSONObject(renameString); - String newName = renameJson.getString("NewName"); + String newName = renameJson.getString(NEW_NAME); file.setFileName(newName); } catch (JSONException e) { Log_OC.e(this, "Failed to parse rename json message: " + e); @@ -422,7 +457,7 @@ public class RichDocumentsWebView extends ExternalSiteWebView { @JavascriptInterface public void paste() { - // Javascript cannot do this by itself, so help out. + // Javascript cannot do this by itself, so help out. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { webview.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE)); webview.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE)); diff --git a/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java new file mode 100644 index 0000000000..3fe87eb98a --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/adapter/PrintAdapter.java @@ -0,0 +1,110 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.adapter; + +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentInfo; + +import com.owncloud.android.lib.common.utils.Log_OC; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +import androidx.annotation.RequiresApi; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class PrintAdapter extends PrintDocumentAdapter { + private static final String TAG = PrintAdapter.class.getSimpleName(); + private static final String PDF_NAME = "finalPrint.pdf"; + + private String filePath; + + public PrintAdapter(String filePath) { + this.filePath = filePath; + } + + @Override + public void onLayout(PrintAttributes oldAttributes, + PrintAttributes newAttributes, + CancellationSignal cancellationSignal, + LayoutResultCallback callback, + Bundle extras) { + if (cancellationSignal.isCanceled()) { + callback.onLayoutCancelled(); + } else { + PrintDocumentInfo.Builder builder = new PrintDocumentInfo.Builder(PDF_NAME); + builder.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT) + .setPageCount(PrintDocumentInfo.PAGE_COUNT_UNKNOWN) + .build(); + callback.onLayoutFinished(builder.build(), !newAttributes.equals(oldAttributes)); + } + } + + + @Override + public void onWrite(PageRange[] pages, + ParcelFileDescriptor destination, + CancellationSignal cancellationSignal, + WriteResultCallback callback) { + InputStream in = null; + OutputStream out = null; + try { + in = new FileInputStream(new File(filePath)); + out = new FileOutputStream(destination.getFileDescriptor()); + + byte[] buf = new byte[16384]; + int size; + + while ((size = in.read(buf)) >= 0 && !cancellationSignal.isCanceled()) { + out.write(buf, 0, size); + } + + if (cancellationSignal.isCanceled()) { + callback.onWriteCancelled(); + } else { + callback.onWriteFinished(new PageRange[]{PageRange.ALL_PAGES}); + } + + } catch (IOException e) { + Log_OC.e(TAG, "Error using temp file", e); + } finally { + try { + Objects.requireNonNull(in).close(); + Objects.requireNonNull(out).close(); + + } catch (IOException | NullPointerException e) { + Log_OC.e(TAG, "Error closing streams", e); + } + } + } +} diff --git a/src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java b/src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java new file mode 100644 index 0000000000..5b8ebbad1a --- /dev/null +++ b/src/main/java/com/owncloud/android/ui/asynctasks/PrintAsyncTask.java @@ -0,0 +1,150 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.asynctasks; + +import android.os.AsyncTask; +import android.os.Build; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintManager; + +import com.owncloud.android.R; +import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.ui.activity.RichDocumentsWebView; +import com.owncloud.android.ui.adapter.PrintAdapter; +import com.owncloud.android.utils.DisplayUtils; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.GetMethod; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; + +import androidx.annotation.RequiresApi; + +import static android.content.Context.PRINT_SERVICE; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class PrintAsyncTask extends AsyncTask { + private static final String TAG = PrintAsyncTask.class.getSimpleName(); + private static final String JOB_NAME = "Document"; + + private File file; + private String url; + private WeakReference richDocumentsWebViewWeakReference; + + public PrintAsyncTask(File file, String url, WeakReference richDocumentsWebViewWeakReference) { + this.file = file; + this.url = url; + this.richDocumentsWebViewWeakReference = richDocumentsWebViewWeakReference; + } + + @Override + protected void onPreExecute() { + richDocumentsWebViewWeakReference.get().runOnUiThread( + () -> richDocumentsWebViewWeakReference.get().showLoadingDialog( + richDocumentsWebViewWeakReference.get().getString(R.string.common_loading))); + + super.onPreExecute(); + } + + @Override + protected Boolean doInBackground(Void... voids) { + HttpClient client = new HttpClient(); + GetMethod getMethod = new GetMethod(url); + + FileOutputStream fos; + try { + int status = client.executeMethod(getMethod); + if (status == HttpStatus.SC_OK) { + if (file.exists()) { + if (!file.delete()) { + return false; + } + } + + file.getParentFile().mkdirs(); + + if (!file.getParentFile().exists()) { + Log_OC.d(TAG, file.getParentFile().getAbsolutePath() + " does not exist"); + return false; + } + + if (!file.createNewFile()) { + Log_OC.d(TAG, file.getAbsolutePath() + " could not be created"); + return false; + } + + BufferedInputStream bis = new BufferedInputStream(getMethod.getResponseBodyAsStream()); + fos = new FileOutputStream(file); + long transferred = 0; + + Header contentLength = getMethod.getResponseHeader("Content-Length"); + long totalToTransfer = contentLength != null && contentLength.getValue().length() > 0 ? + Long.parseLong(contentLength.getValue()) : 0; + + byte[] bytes = new byte[4096]; + int readResult; + while ((readResult = bis.read(bytes)) != -1) { + fos.write(bytes, 0, readResult); + transferred += readResult; + } + // Check if the file is completed + if (transferred != totalToTransfer) { + return false; + } + + if (getMethod.getResponseBodyAsStream() != null) { + getMethod.getResponseBodyAsStream().close(); + } + } + } catch (IOException e) { + Log_OC.e(TAG, "Error reading file", e); + } + + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + RichDocumentsWebView richDocumentsWebView = richDocumentsWebViewWeakReference.get(); + richDocumentsWebView.dismissLoadingDialog(); + + PrintManager printManager = (PrintManager) richDocumentsWebView.getSystemService(PRINT_SERVICE); + + if (!result || printManager == null) { + DisplayUtils.showSnackMessage(richDocumentsWebView, + richDocumentsWebView.getString(R.string.failed_to_print)); + + return; + } + + PrintDocumentAdapter printAdapter = new PrintAdapter(file.getAbsolutePath()); + + printManager.print(JOB_NAME, printAdapter, new PrintAttributes.Builder().build()); + } +} diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index dc20f454e9..504ecc58ec 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -884,6 +884,7 @@ Copy internal link Only works for users with access to this folder Failed to pass file to download manager + Failed to print file Disable power save check Disabling power save check might result in uploading files when in low battery state! Engineering Test Mode