PreviewImageFragment.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. /* ownCloud Android client application
  2. * Copyright (C) 2012-2013 ownCloud Inc.
  3. *
  4. * This program is free software: you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License version 2,
  6. * as published by the Free Software Foundation.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License
  14. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. *
  16. */
  17. package com.owncloud.android.ui.preview;
  18. import java.io.File;
  19. import java.lang.ref.WeakReference;
  20. import java.util.ArrayList;
  21. import java.util.List;
  22. import android.accounts.Account;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.content.ActivityNotFoundException;
  26. import android.content.Intent;
  27. import android.graphics.Bitmap;
  28. import android.graphics.BitmapFactory;
  29. import android.graphics.BitmapFactory.Options;
  30. import android.graphics.Point;
  31. import android.net.Uri;
  32. import android.os.AsyncTask;
  33. import android.os.Bundle;
  34. import android.os.Handler;
  35. import android.support.v4.app.FragmentStatePagerAdapter;
  36. import android.view.Display;
  37. import android.view.LayoutInflater;
  38. import android.view.View;
  39. import android.view.View.OnTouchListener;
  40. import android.view.ViewGroup;
  41. import android.webkit.MimeTypeMap;
  42. import android.widget.ImageView;
  43. import android.widget.ProgressBar;
  44. import android.widget.TextView;
  45. import android.widget.Toast;
  46. import com.actionbarsherlock.view.Menu;
  47. import com.actionbarsherlock.view.MenuInflater;
  48. import com.actionbarsherlock.view.MenuItem;
  49. import com.owncloud.android.datamodel.FileDataStorageManager;
  50. import com.owncloud.android.datamodel.OCFile;
  51. import com.owncloud.android.operations.OnRemoteOperationListener;
  52. import com.owncloud.android.operations.RemoteOperation;
  53. import com.owncloud.android.operations.RemoteOperationResult;
  54. import com.owncloud.android.operations.RemoveFileOperation;
  55. import com.owncloud.android.ui.fragment.ConfirmationDialogFragment;
  56. import com.owncloud.android.ui.fragment.FileFragment;
  57. import com.owncloud.android.Log_OC;
  58. import com.owncloud.android.R;
  59. import eu.alefzero.webdav.WebdavUtils;
  60. /**
  61. * This fragment shows a preview of a downloaded image.
  62. *
  63. * Trying to get an instance with NULL {@link OCFile} or ownCloud {@link Account} values will produce an {@link IllegalStateException}.
  64. *
  65. * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on instantiation too.
  66. *
  67. * @author David A. Velasco
  68. */
  69. public class PreviewImageFragment extends FileFragment implements OnRemoteOperationListener,
  70. ConfirmationDialogFragment.ConfirmationDialogFragmentListener {
  71. public static final String EXTRA_FILE = "FILE";
  72. public static final String EXTRA_ACCOUNT = "ACCOUNT";
  73. private View mView;
  74. private Account mAccount;
  75. private FileDataStorageManager mStorageManager;
  76. private ImageView mImageView;
  77. private TextView mMessageView;
  78. private ProgressBar mProgressWheel;
  79. public Bitmap mBitmap = null;
  80. private Handler mHandler;
  81. private RemoteOperation mLastRemoteOperation;
  82. private static final String TAG = PreviewImageFragment.class.getSimpleName();
  83. private boolean mIgnoreFirstSavedState;
  84. /**
  85. * Creates a fragment to preview an image.
  86. *
  87. * When 'imageFile' or 'ocAccount' are null
  88. *
  89. * @param imageFile An {@link OCFile} to preview as an image in the fragment
  90. * @param ocAccount An ownCloud account; needed to start downloads
  91. * @param ignoreFirstSavedState Flag to work around an unexpected behaviour of {@link FragmentStatePagerAdapter}; TODO better solution
  92. */
  93. public PreviewImageFragment(OCFile fileToDetail, Account ocAccount, boolean ignoreFirstSavedState) {
  94. super(fileToDetail);
  95. mAccount = ocAccount;
  96. mStorageManager = null; // we need a context to init this; the container activity is not available yet at this moment
  97. mIgnoreFirstSavedState = ignoreFirstSavedState;
  98. }
  99. /**
  100. * Creates an empty fragment for image previews.
  101. *
  102. * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically (for instance, when the device is turned a aside).
  103. *
  104. * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful construction
  105. */
  106. public PreviewImageFragment() {
  107. super();
  108. mAccount = null;
  109. mStorageManager = null;
  110. mIgnoreFirstSavedState = false;
  111. }
  112. /**
  113. * {@inheritDoc}
  114. */
  115. @Override
  116. public void onCreate(Bundle savedInstanceState) {
  117. super.onCreate(savedInstanceState);
  118. mHandler = new Handler();
  119. setHasOptionsMenu(true);
  120. }
  121. /**
  122. * {@inheritDoc}
  123. */
  124. @Override
  125. public View onCreateView(LayoutInflater inflater, ViewGroup container,
  126. Bundle savedInstanceState) {
  127. super.onCreateView(inflater, container, savedInstanceState);
  128. mView = inflater.inflate(R.layout.preview_image_fragment, container, false);
  129. mImageView = (ImageView)mView.findViewById(R.id.image);
  130. mImageView.setVisibility(View.GONE);
  131. mView.setOnTouchListener((OnTouchListener)getActivity()); // WATCH OUT THAT CAST
  132. mMessageView = (TextView)mView.findViewById(R.id.message);
  133. mMessageView.setVisibility(View.GONE);
  134. mProgressWheel = (ProgressBar)mView.findViewById(R.id.progressWheel);
  135. mProgressWheel.setVisibility(View.VISIBLE);
  136. return mView;
  137. }
  138. /**
  139. * {@inheritDoc}
  140. */
  141. @Override
  142. public void onAttach(Activity activity) {
  143. super.onAttach(activity);
  144. if (!(activity instanceof FileFragment.ContainerActivity))
  145. throw new ClassCastException(activity.toString() + " must implement " + FileFragment.ContainerActivity.class.getSimpleName());
  146. }
  147. /**
  148. * {@inheritDoc}
  149. */
  150. @Override
  151. public void onActivityCreated(Bundle savedInstanceState) {
  152. super.onActivityCreated(savedInstanceState);
  153. mStorageManager = new FileDataStorageManager(mAccount, getActivity().getApplicationContext().getContentResolver());
  154. if (savedInstanceState != null) {
  155. if (!mIgnoreFirstSavedState) {
  156. setFile((OCFile)savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE));
  157. mAccount = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_ACCOUNT);
  158. } else {
  159. mIgnoreFirstSavedState = false;
  160. }
  161. }
  162. if (getFile() == null) {
  163. throw new IllegalStateException("Instanced with a NULL OCFile");
  164. }
  165. if (mAccount == null) {
  166. throw new IllegalStateException("Instanced with a NULL ownCloud Account");
  167. }
  168. if (!getFile().isDown()) {
  169. throw new IllegalStateException("There is no local file to preview");
  170. }
  171. }
  172. /**
  173. * {@inheritDoc}
  174. */
  175. @Override
  176. public void onSaveInstanceState(Bundle outState) {
  177. super.onSaveInstanceState(outState);
  178. outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
  179. outState.putParcelable(PreviewImageFragment.EXTRA_ACCOUNT, mAccount);
  180. }
  181. @Override
  182. public void onStart() {
  183. super.onStart();
  184. if (getFile() != null) {
  185. BitmapLoader bl = new BitmapLoader(mImageView, mMessageView, mProgressWheel);
  186. bl.execute(new String[]{getFile().getStoragePath()});
  187. }
  188. }
  189. /**
  190. * {@inheritDoc}
  191. */
  192. @Override
  193. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  194. super.onCreateOptionsMenu(menu, inflater);
  195. inflater.inflate(R.menu.file_actions_menu, menu);
  196. List<Integer> toHide = new ArrayList<Integer>();
  197. MenuItem item = null;
  198. toHide.add(R.id.action_cancel_download);
  199. toHide.add(R.id.action_cancel_upload);
  200. toHide.add(R.id.action_download_file);
  201. toHide.add(R.id.action_rename_file); // by now
  202. for (int i : toHide) {
  203. item = menu.findItem(i);
  204. if (item != null) {
  205. item.setVisible(false);
  206. item.setEnabled(false);
  207. }
  208. }
  209. }
  210. /**
  211. * {@inheritDoc}
  212. */
  213. @Override
  214. public boolean onOptionsItemSelected(MenuItem item) {
  215. switch (item.getItemId()) {
  216. case R.id.action_open_file_with: {
  217. openFile();
  218. return true;
  219. }
  220. case R.id.action_remove_file: {
  221. removeFile();
  222. return true;
  223. }
  224. case R.id.action_see_details: {
  225. seeDetails();
  226. return true;
  227. }
  228. default:
  229. return false;
  230. }
  231. }
  232. private void seeDetails() {
  233. ((FileFragment.ContainerActivity)getActivity()).showDetails(getFile());
  234. }
  235. @Override
  236. public void onResume() {
  237. super.onResume();
  238. }
  239. @Override
  240. public void onPause() {
  241. super.onPause();
  242. }
  243. @Override
  244. public void onDestroy() {
  245. super.onDestroy();
  246. if (mBitmap != null) {
  247. mBitmap.recycle();
  248. }
  249. }
  250. /**
  251. * Opens the previewed image with an external application.
  252. *
  253. * TODO - improve this; instead of prioritize the actions available for the MIME type in the server,
  254. * we should get a list of available apps for MIME tpye in the server and join it with the list of
  255. * available apps for the MIME type known from the file extension, to let the user choose
  256. */
  257. private void openFile() {
  258. OCFile file = getFile();
  259. String storagePath = file.getStoragePath();
  260. String encodedStoragePath = WebdavUtils.encodePath(storagePath);
  261. try {
  262. Intent i = new Intent(Intent.ACTION_VIEW);
  263. i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), file.getMimetype());
  264. i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  265. startActivity(i);
  266. } catch (Throwable t) {
  267. Log_OC.e(TAG, "Fail when trying to open with the mimeType provided from the ownCloud server: " + file.getMimetype());
  268. boolean toastIt = true;
  269. String mimeType = "";
  270. try {
  271. Intent i = new Intent(Intent.ACTION_VIEW);
  272. mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(storagePath.substring(storagePath.lastIndexOf('.') + 1));
  273. if (mimeType == null || !mimeType.equals(file.getMimetype())) {
  274. if (mimeType != null) {
  275. i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), mimeType);
  276. } else {
  277. // desperate try
  278. i.setDataAndType(Uri.parse("file://"+ encodedStoragePath), "*-/*");
  279. }
  280. i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  281. startActivity(i);
  282. toastIt = false;
  283. }
  284. } catch (IndexOutOfBoundsException e) {
  285. Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " + storagePath);
  286. } catch (ActivityNotFoundException e) {
  287. Log_OC.e(TAG, "No activity found to handle: " + storagePath + " with MIME type " + mimeType + " obtained from extension");
  288. } catch (Throwable th) {
  289. Log_OC.e(TAG, "Unexpected problem when opening: " + storagePath, th);
  290. } finally {
  291. if (toastIt) {
  292. Toast.makeText(getActivity(), "There is no application to handle file " + file.getFileName(), Toast.LENGTH_SHORT).show();
  293. }
  294. }
  295. }
  296. finish();
  297. }
  298. /**
  299. * Starts a the removal of the previewed file.
  300. *
  301. * Shows a confirmation dialog. The action continues in {@link #onConfirmation(String)} , {@link #onNeutral(String)} or {@link #onCancel(String)},
  302. * depending upon the user selection in the dialog.
  303. */
  304. private void removeFile() {
  305. ConfirmationDialogFragment confDialog = ConfirmationDialogFragment.newInstance(
  306. R.string.confirmation_remove_alert,
  307. new String[]{getFile().getFileName()},
  308. R.string.confirmation_remove_remote_and_local,
  309. R.string.confirmation_remove_local,
  310. R.string.common_cancel);
  311. confDialog.setOnConfirmationListener(this);
  312. confDialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
  313. }
  314. /**
  315. * Performs the removal of the previewed file, both locally and in the server.
  316. */
  317. @Override
  318. public void onConfirmation(String callerTag) {
  319. if (mStorageManager.getFileById(getFile().getFileId()) != null) { // check that the file is still there;
  320. mLastRemoteOperation = new RemoveFileOperation( getFile(), // TODO we need to review the interface with RemoteOperations, and use OCFile IDs instead of OCFile objects as parameters
  321. true,
  322. mStorageManager);
  323. mLastRemoteOperation.execute(mAccount, getSherlockActivity(), this, mHandler, getSherlockActivity());
  324. ((PreviewImageActivity) getActivity()).showLoadingDialog();
  325. }
  326. }
  327. /**
  328. * Removes the file from local storage
  329. */
  330. @Override
  331. public void onNeutral(String callerTag) {
  332. // TODO this code should be made in a secondary thread,
  333. OCFile file = getFile();
  334. if (file.isDown()) { // checks it is still there
  335. File f = new File(file.getStoragePath());
  336. f.delete();
  337. file.setStoragePath(null);
  338. mStorageManager.saveFile(file);
  339. finish();
  340. }
  341. }
  342. /**
  343. * User cancelled the removal action.
  344. */
  345. @Override
  346. public void onCancel(String callerTag) {
  347. // nothing to do here
  348. }
  349. private class BitmapLoader extends AsyncTask<String, Void, Bitmap> {
  350. /**
  351. * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
  352. *
  353. * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
  354. */
  355. private final WeakReference<ImageView> mImageViewRef;
  356. /**
  357. * Weak reference to the target {@link TextView} where error messages will be written.
  358. *
  359. * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
  360. */
  361. private final WeakReference<TextView> mMessageViewRef;
  362. /**
  363. * Weak reference to the target {@link Progressbar} shown while the load is in progress.
  364. *
  365. * Using a weak reference will avoid memory leaks if the target ImageView is retired from memory before the load finishes.
  366. */
  367. private final WeakReference<ProgressBar> mProgressWheelRef;
  368. /**
  369. * Error message to show when a load fails
  370. */
  371. private int mErrorMessageId;
  372. /**
  373. * Constructor.
  374. *
  375. * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
  376. */
  377. public BitmapLoader(ImageView imageView, TextView messageView, ProgressBar progressWheel) {
  378. mImageViewRef = new WeakReference<ImageView>(imageView);
  379. mMessageViewRef = new WeakReference<TextView>(messageView);
  380. mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
  381. }
  382. @SuppressWarnings("deprecation")
  383. @SuppressLint({ "NewApi", "NewApi", "NewApi" }) // to avoid Lint errors since Android SDK r20
  384. @Override
  385. protected Bitmap doInBackground(String... params) {
  386. Bitmap result = null;
  387. if (params.length != 1) return result;
  388. String storagePath = params[0];
  389. try {
  390. // set desired options that will affect the size of the bitmap
  391. BitmapFactory.Options options = new Options();
  392. options.inScaled = true;
  393. options.inPurgeable = true;
  394. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD_MR1) {
  395. options.inPreferQualityOverSpeed = false;
  396. }
  397. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
  398. options.inMutable = false;
  399. }
  400. // make a false load of the bitmap - just to be able to read outWidth, outHeight and outMimeType
  401. options.inJustDecodeBounds = true;
  402. BitmapFactory.decodeFile(storagePath, options);
  403. int width = options.outWidth;
  404. int height = options.outHeight;
  405. int scale = 1;
  406. Display display = getActivity().getWindowManager().getDefaultDisplay();
  407. Point size = new Point();
  408. int screenWidth;
  409. int screenHeight;
  410. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR2) {
  411. display.getSize(size);
  412. screenWidth = size.x;
  413. screenHeight = size.y;
  414. } else {
  415. screenWidth = display.getWidth();
  416. screenHeight = display.getHeight();
  417. }
  418. if (width > screenWidth) {
  419. // second try to scale down the image , this time depending upon the screen size
  420. scale = (int) Math.floor((float)width / screenWidth);
  421. }
  422. if (height > screenHeight) {
  423. scale = Math.max(scale, (int) Math.floor((float)height / screenHeight));
  424. }
  425. options.inSampleSize = scale;
  426. // really load the bitmap
  427. options.inJustDecodeBounds = false; // the next decodeFile call will be real
  428. result = BitmapFactory.decodeFile(storagePath, options);
  429. //Log_OC.d(TAG, "Image loaded - width: " + options.outWidth + ", loaded height: " + options.outHeight);
  430. if (result == null) {
  431. mErrorMessageId = R.string.preview_image_error_unknown_format;
  432. Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
  433. }
  434. } catch (OutOfMemoryError e) {
  435. mErrorMessageId = R.string.preview_image_error_unknown_format;
  436. Log_OC.e(TAG, "Out of memory occured for file " + storagePath, e);
  437. } catch (NoSuchFieldError e) {
  438. mErrorMessageId = R.string.common_error_unknown;
  439. Log_OC.e(TAG, "Error from access to unexisting field despite protection; file " + storagePath, e);
  440. } catch (Throwable t) {
  441. mErrorMessageId = R.string.common_error_unknown;
  442. Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
  443. }
  444. return result;
  445. }
  446. @Override
  447. protected void onPostExecute(Bitmap result) {
  448. hideProgressWheel();
  449. if (result != null) {
  450. showLoadedImage(result);
  451. } else {
  452. showErrorMessage();
  453. }
  454. }
  455. private void showLoadedImage(Bitmap result) {
  456. if (mImageViewRef != null) {
  457. final ImageView imageView = mImageViewRef.get();
  458. if (imageView != null) {
  459. imageView.setImageBitmap(result);
  460. imageView.setVisibility(View.VISIBLE);
  461. mBitmap = result;
  462. } // else , silently finish, the fragment was destroyed
  463. }
  464. if (mMessageViewRef != null) {
  465. final TextView messageView = mMessageViewRef.get();
  466. if (messageView != null) {
  467. messageView.setVisibility(View.GONE);
  468. } // else , silently finish, the fragment was destroyed
  469. }
  470. }
  471. private void showErrorMessage() {
  472. if (mImageViewRef != null) {
  473. final ImageView imageView = mImageViewRef.get();
  474. if (imageView != null) {
  475. // shows the default error icon
  476. imageView.setVisibility(View.VISIBLE);
  477. } // else , silently finish, the fragment was destroyed
  478. }
  479. if (mMessageViewRef != null) {
  480. final TextView messageView = mMessageViewRef.get();
  481. if (messageView != null) {
  482. messageView.setText(mErrorMessageId);
  483. messageView.setVisibility(View.VISIBLE);
  484. } // else , silently finish, the fragment was destroyed
  485. }
  486. }
  487. private void hideProgressWheel() {
  488. if (mProgressWheelRef != null) {
  489. final ProgressBar progressWheel = mProgressWheelRef.get();
  490. if (progressWheel != null) {
  491. progressWheel.setVisibility(View.GONE);
  492. }
  493. }
  494. }
  495. }
  496. /**
  497. * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment} to be previewed.
  498. *
  499. * @param file File to test if can be previewed.
  500. * @return 'True' if the file can be handled by the fragment.
  501. */
  502. public static boolean canBePreviewed(OCFile file) {
  503. return (file != null && file.isImage());
  504. }
  505. /**
  506. * {@inheritDoc}
  507. */
  508. @Override
  509. public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
  510. if (operation.equals(mLastRemoteOperation) && operation instanceof RemoveFileOperation) {
  511. onRemoveFileOperationFinish((RemoveFileOperation)operation, result);
  512. }
  513. }
  514. private void onRemoveFileOperationFinish(RemoveFileOperation operation, RemoteOperationResult result) {
  515. ((PreviewImageActivity) getActivity()).dismissLoadingDialog();
  516. if (result.isSuccess()) {
  517. Toast msg = Toast.makeText(getActivity().getApplicationContext(), R.string.remove_success_msg, Toast.LENGTH_LONG);
  518. msg.show();
  519. finish();
  520. } else {
  521. Toast msg = Toast.makeText(getActivity(), R.string.remove_fail_msg, Toast.LENGTH_LONG);
  522. msg.show();
  523. if (result.isSslRecoverableException()) {
  524. // TODO show the SSL warning dialog
  525. }
  526. }
  527. }
  528. /**
  529. * Finishes the preview
  530. */
  531. private void finish() {
  532. Activity container = getActivity();
  533. container.finish();
  534. }
  535. }