RichDocumentsWebView.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Tobias Kaminsky
  5. * @author Chris Narkiewicz
  6. *
  7. * Copyright (C) 2018 Tobias Kaminsky
  8. * Copyright (C) 2018 Nextcloud GmbH.
  9. * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License as published by
  13. * the Free Software Foundation, either version 3 of the License, or
  14. * (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU General Public License
  22. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  23. */
  24. package com.owncloud.android.ui.activity;
  25. import android.accounts.Account;
  26. import android.annotation.SuppressLint;
  27. import android.app.DownloadManager;
  28. import android.content.ActivityNotFoundException;
  29. import android.content.Context;
  30. import android.content.Intent;
  31. import android.graphics.Bitmap;
  32. import android.net.Uri;
  33. import android.os.Build;
  34. import android.os.Bundle;
  35. import android.text.TextUtils;
  36. import android.view.KeyEvent;
  37. import android.view.View;
  38. import android.webkit.JavascriptInterface;
  39. import android.webkit.ValueCallback;
  40. import android.webkit.WebChromeClient;
  41. import android.webkit.WebView;
  42. import android.widget.ImageView;
  43. import android.widget.ProgressBar;
  44. import android.widget.TextView;
  45. import android.widget.Toast;
  46. import com.bumptech.glide.Glide;
  47. import com.google.android.material.snackbar.Snackbar;
  48. import com.nextcloud.client.account.CurrentAccountProvider;
  49. import com.nextcloud.client.network.ClientFactory;
  50. import com.owncloud.android.R;
  51. import com.owncloud.android.datamodel.OCFile;
  52. import com.owncloud.android.datamodel.Template;
  53. import com.owncloud.android.datamodel.ThumbnailsCacheManager;
  54. import com.owncloud.android.lib.common.OwnCloudAccount;
  55. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  56. import com.owncloud.android.lib.common.utils.Log_OC;
  57. import com.owncloud.android.operations.RichDocumentsCreateAssetOperation;
  58. import com.owncloud.android.ui.asynctasks.LoadUrlTask;
  59. import com.owncloud.android.ui.asynctasks.PrintAsyncTask;
  60. import com.owncloud.android.ui.fragment.OCFileListFragment;
  61. import com.owncloud.android.utils.DisplayUtils;
  62. import com.owncloud.android.utils.FileStorageUtils;
  63. import com.owncloud.android.utils.MimeTypeUtil;
  64. import com.owncloud.android.utils.glide.CustomGlideStreamLoader;
  65. import org.json.JSONException;
  66. import org.json.JSONObject;
  67. import org.parceler.Parcels;
  68. import java.io.File;
  69. import java.lang.ref.WeakReference;
  70. import javax.inject.Inject;
  71. import androidx.annotation.RequiresApi;
  72. import butterknife.BindView;
  73. import butterknife.ButterKnife;
  74. import butterknife.Unbinder;
  75. import lombok.Getter;
  76. import lombok.Setter;
  77. /**
  78. * Opens document for editing via Richdocuments app in a web view
  79. */
  80. @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  81. public class RichDocumentsWebView extends ExternalSiteWebView {
  82. public static final int MINIMUM_API = Build.VERSION_CODES.LOLLIPOP;
  83. public static final int REQUEST_LOCAL_FILE = 101;
  84. private static final int REQUEST_REMOTE_FILE = 100;
  85. private static final String TAG = RichDocumentsWebView.class.getSimpleName();
  86. private static final String URL = "URL";
  87. private static final String TYPE = "Type";
  88. private static final String PRINT = "print";
  89. private static final String NEW_NAME = "NewName";
  90. private Unbinder unbinder;
  91. private OCFile file;
  92. @Getter @Setter private Snackbar loadingSnackbar;
  93. public ValueCallback<Uri[]> uploadMessage;
  94. @BindView(R.id.progressBar2)
  95. ProgressBar progressBar;
  96. @BindView(R.id.thumbnail)
  97. ImageView thumbnail;
  98. @BindView(R.id.filename)
  99. TextView fileName;
  100. @Inject
  101. protected CurrentAccountProvider currentAccountProvider;
  102. @Inject
  103. protected ClientFactory clientFactory;
  104. @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used >= Lollipop
  105. @Override
  106. protected void onCreate(Bundle savedInstanceState) {
  107. showToolbar = false;
  108. webViewLayout = R.layout.richdocuments_webview;
  109. super.onCreate(savedInstanceState);
  110. unbinder = ButterKnife.bind(this);
  111. file = getIntent().getParcelableExtra(EXTRA_FILE);
  112. // TODO make file nullable
  113. if (file == null) {
  114. fileName.setText(R.string.create_file_from_template);
  115. Template template = Parcels.unwrap(getIntent().getParcelableExtra(EXTRA_TEMPLATE));
  116. int placeholder;
  117. switch (template.getType()) {
  118. case "document":
  119. placeholder = R.drawable.file_doc;
  120. break;
  121. case "spreadsheet":
  122. placeholder = R.drawable.file_xls;
  123. break;
  124. case "presentation":
  125. placeholder = R.drawable.file_ppt;
  126. break;
  127. default:
  128. placeholder = R.drawable.file;
  129. break;
  130. }
  131. Glide.with(this).using(new CustomGlideStreamLoader(currentAccountProvider)).load(template.getThumbnailLink())
  132. .placeholder(placeholder)
  133. .error(placeholder)
  134. .into(thumbnail);
  135. } else {
  136. setThumbnail(file, thumbnail);
  137. fileName.setText(file.getFileName());
  138. }
  139. webview.addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface");
  140. webview.setWebChromeClient(new WebChromeClient() {
  141. RichDocumentsWebView activity = RichDocumentsWebView.this;
  142. @Override
  143. public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
  144. FileChooserParams fileChooserParams) {
  145. if (uploadMessage != null) {
  146. uploadMessage.onReceiveValue(null);
  147. uploadMessage = null;
  148. }
  149. activity.uploadMessage = filePathCallback;
  150. Intent intent = fileChooserParams.createIntent();
  151. intent.setType("image/*");
  152. intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
  153. try {
  154. activity.startActivityForResult(intent, REQUEST_LOCAL_FILE);
  155. } catch (ActivityNotFoundException e) {
  156. uploadMessage = null;
  157. Toast.makeText(getBaseContext(), "Cannot open file chooser", Toast.LENGTH_LONG).show();
  158. return false;
  159. }
  160. return true;
  161. }
  162. });
  163. // load url in background
  164. url = getIntent().getStringExtra(EXTRA_URL);
  165. if (TextUtils.isEmpty(url)) {
  166. new LoadUrlTask(this, getAccount()).execute(file.getLocalId());
  167. } else {
  168. webview.loadUrl(url);
  169. }
  170. }
  171. private void setThumbnail(OCFile file, ImageView thumbnailView) {
  172. // Todo minimize: only icon by mimetype
  173. if (file.isFolder()) {
  174. thumbnailView.setImageDrawable(MimeTypeUtil.getFolderTypeIcon(file.isSharedWithMe() ||
  175. file.isSharedWithSharee(), file.isSharedViaLink(), file.isEncrypted(), file.getMountType(),
  176. this));
  177. } else {
  178. if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) {
  179. // Thumbnail in cache?
  180. Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
  181. ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId());
  182. if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
  183. if (MimeTypeUtil.isVideo(file)) {
  184. Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
  185. thumbnailView.setImageBitmap(withOverlay);
  186. } else {
  187. thumbnailView.setImageBitmap(thumbnail);
  188. }
  189. } else {
  190. // generate new thumbnail
  191. if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
  192. try {
  193. final ThumbnailsCacheManager.ThumbnailGenerationTask task =
  194. new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
  195. getStorageManager(), getAccount());
  196. if (thumbnail == null) {
  197. if (MimeTypeUtil.isVideo(file)) {
  198. thumbnail = ThumbnailsCacheManager.mDefaultVideo;
  199. } else {
  200. thumbnail = ThumbnailsCacheManager.mDefaultImg;
  201. }
  202. }
  203. final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
  204. new ThumbnailsCacheManager.AsyncThumbnailDrawable(getResources(), thumbnail, task);
  205. thumbnailView.setImageDrawable(asyncDrawable);
  206. task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
  207. file.getRemoteId()));
  208. } catch (IllegalArgumentException e) {
  209. Log_OC.d(TAG, "ThumbnailGenerationTask : " + e.getMessage());
  210. }
  211. }
  212. }
  213. if ("image/png".equalsIgnoreCase(file.getMimeType())) {
  214. thumbnailView.setBackgroundColor(getResources().getColor(R.color.bg_default));
  215. }
  216. } else {
  217. thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), file.getFileName(),
  218. getAccount(), this));
  219. }
  220. }
  221. }
  222. @Override
  223. protected void onNewIntent(Intent intent) {
  224. super.onNewIntent(intent);
  225. }
  226. private void openFileChooser() {
  227. Intent action = new Intent(this, FilePickerActivity.class);
  228. action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/");
  229. startActivityForResult(action, REQUEST_REMOTE_FILE);
  230. }
  231. private void openShareDialog() {
  232. Intent intent = new Intent(this, ShareActivity.class);
  233. intent.putExtra(FileActivity.EXTRA_FILE, file);
  234. intent.putExtra(FileActivity.EXTRA_ACCOUNT, getAccount());
  235. startActivity(intent);
  236. }
  237. @Override
  238. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  239. if (RESULT_OK != resultCode) {
  240. // TODO
  241. return;
  242. }
  243. switch (requestCode) {
  244. case REQUEST_LOCAL_FILE:
  245. handleLocalFile(data, resultCode);
  246. break;
  247. case REQUEST_REMOTE_FILE:
  248. handleRemoteFile(data);
  249. break;
  250. default:
  251. // unexpected, do nothing
  252. break;
  253. }
  254. super.onActivityResult(requestCode, resultCode, data);
  255. }
  256. private void handleLocalFile(Intent data, int resultCode) {
  257. if (uploadMessage == null) {
  258. return;
  259. }
  260. uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
  261. uploadMessage = null;
  262. }
  263. private void handleRemoteFile(Intent data) {
  264. OCFile file = data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES);
  265. new Thread(() -> {
  266. Account account = currentAccountProvider.getCurrentAccount();
  267. RichDocumentsCreateAssetOperation operation = new RichDocumentsCreateAssetOperation(file.getRemotePath());
  268. RemoteOperationResult result = operation.execute(account, this);
  269. if (result.isSuccess()) {
  270. String asset = (String) result.getSingleData();
  271. runOnUiThread(() -> webview.evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" +
  272. file.getFileName() + "', '" + asset + "');", null));
  273. } else {
  274. runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!"));
  275. }
  276. }).start();
  277. }
  278. @Override
  279. protected void onSaveInstanceState(Bundle outState) {
  280. outState.putString(EXTRA_URL, url);
  281. super.onSaveInstanceState(outState);
  282. }
  283. @Override
  284. public void onRestoreInstanceState(Bundle savedInstanceState) {
  285. url = savedInstanceState.getString(EXTRA_URL);
  286. super.onRestoreInstanceState(savedInstanceState);
  287. }
  288. @Override
  289. protected void onDestroy() {
  290. unbinder.unbind();
  291. webview.destroy();
  292. super.onDestroy();
  293. }
  294. public void closeView() {
  295. webview.destroy();
  296. finish();
  297. }
  298. private void hideLoading() {
  299. thumbnail.setVisibility(View.GONE);
  300. fileName.setVisibility(View.GONE);
  301. progressBar.setVisibility(View.GONE);
  302. webview.setVisibility(View.VISIBLE);
  303. if (loadingSnackbar != null) {
  304. loadingSnackbar.dismiss();
  305. }
  306. }
  307. @Override
  308. protected void onResume() {
  309. super.onResume();
  310. webview.evaluateJavascript("if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " +
  311. "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", null);
  312. }
  313. private void printFile(Uri url) {
  314. OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount();
  315. if (account == null) {
  316. DisplayUtils.showSnackMessage(webview, getString(R.string.failed_to_print));
  317. return;
  318. }
  319. File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf");
  320. new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute();
  321. }
  322. private void downloadFile(Uri url) {
  323. DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
  324. if (downloadmanager == null) {
  325. DisplayUtils.showSnackMessage(webview, getString(R.string.failed_to_download));
  326. return;
  327. }
  328. DownloadManager.Request request = new DownloadManager.Request(url);
  329. request.allowScanningByMediaScanner();
  330. request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
  331. downloadmanager.enqueue(request);
  332. }
  333. private class RichDocumentsMobileInterface {
  334. @JavascriptInterface
  335. public void close() {
  336. runOnUiThread(RichDocumentsWebView.this::closeView);
  337. }
  338. @JavascriptInterface
  339. public void insertGraphic() {
  340. openFileChooser();
  341. }
  342. @JavascriptInterface
  343. public void share() {
  344. openShareDialog();
  345. }
  346. @JavascriptInterface
  347. public void documentLoaded() {
  348. runOnUiThread(RichDocumentsWebView.this::hideLoading);
  349. }
  350. @JavascriptInterface
  351. public void downloadAs(String json) {
  352. try {
  353. JSONObject downloadJson = new JSONObject(json);
  354. Uri url = Uri.parse(downloadJson.getString(URL));
  355. if (downloadJson.getString(TYPE).equalsIgnoreCase(PRINT)) {
  356. printFile(url);
  357. } else {
  358. downloadFile(url);
  359. }
  360. } catch (JSONException e) {
  361. Log_OC.e(this, "Failed to parse download json message: " + e);
  362. return;
  363. }
  364. }
  365. @JavascriptInterface
  366. public void fileRename(String renameString) {
  367. // when shared file is renamed in another instance, we will get notified about it
  368. // need to change filename for sharing
  369. try {
  370. JSONObject renameJson = new JSONObject(renameString);
  371. String newName = renameJson.getString(NEW_NAME);
  372. file.setFileName(newName);
  373. } catch (JSONException e) {
  374. Log_OC.e(this, "Failed to parse rename json message: " + e);
  375. }
  376. }
  377. @JavascriptInterface
  378. public void paste() {
  379. // Javascript cannot do this by itself, so help out.
  380. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  381. webview.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE));
  382. webview.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE));
  383. }
  384. }
  385. }
  386. }