PreviewImageFragment.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. /**
  2. * ownCloud Android client application
  3. *
  4. * @author David A. Velasco
  5. * Copyright (C) 2015 ownCloud Inc.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. package com.owncloud.android.ui.preview;
  21. import android.accounts.Account;
  22. import android.annotation.SuppressLint;
  23. import android.app.Activity;
  24. import android.graphics.Bitmap;
  25. import android.graphics.Point;
  26. import android.graphics.drawable.Drawable;
  27. import android.os.AsyncTask;
  28. import android.os.Bundle;
  29. import android.support.v4.app.FragmentStatePagerAdapter;
  30. import android.view.LayoutInflater;
  31. import android.view.Menu;
  32. import android.view.MenuInflater;
  33. import android.view.MenuItem;
  34. import android.view.View;
  35. import android.view.View.OnClickListener;
  36. import android.view.ViewGroup;
  37. import android.widget.ImageView;
  38. import android.widget.ProgressBar;
  39. import android.widget.TextView;
  40. import com.owncloud.android.R;
  41. import com.owncloud.android.datamodel.OCFile;
  42. import com.owncloud.android.files.FileMenuFilter;
  43. import com.owncloud.android.lib.common.utils.Log_OC;
  44. import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
  45. import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
  46. import com.owncloud.android.ui.fragment.FileFragment;
  47. import com.owncloud.android.utils.BitmapUtils;
  48. import com.owncloud.android.utils.DisplayUtils;
  49. import com.owncloud.android.utils.MimeTypeUtil;
  50. import java.lang.ref.WeakReference;
  51. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  52. import third_parties.michaelOrtiz.TouchImageViewCustom;
  53. /**
  54. * This fragment shows a preview of a downloaded image.
  55. *
  56. * Trying to get an instance with a NULL {@link OCFile} will produce an
  57. * {@link IllegalStateException}.
  58. *
  59. * If the {@link OCFile} passed is not downloaded, an {@link IllegalStateException} is generated on
  60. * instantiation too.
  61. */
  62. public class PreviewImageFragment extends FileFragment {
  63. public static final String EXTRA_FILE = "FILE";
  64. private static final String ARG_FILE = "FILE";
  65. private static final String ARG_IGNORE_FIRST = "IGNORE_FIRST";
  66. private static final String SCREEN_NAME = "Image Preview";
  67. private TouchImageViewCustom mImageView;
  68. private TextView mMessageView;
  69. private ProgressBar mProgressWheel;
  70. public Bitmap mBitmap = null;
  71. private static final String TAG = PreviewImageFragment.class.getSimpleName();
  72. private boolean mIgnoreFirstSavedState;
  73. private LoadBitmapTask mLoadBitmapTask = null;
  74. /**
  75. * Public factory method to create a new fragment that previews an image.
  76. *
  77. * Android strongly recommends keep the empty constructor of fragments as the only public
  78. * constructor, and
  79. * use {@link #setArguments(Bundle)} to set the needed arguments.
  80. *
  81. * This method hides to client objects the need of doing the construction in two steps.
  82. *
  83. * @param imageFile An {@link OCFile} to preview as an image in the fragment
  84. * @param ignoreFirstSavedState Flag to work around an unexpected behaviour of
  85. * {@link FragmentStatePagerAdapter}
  86. * ; TODO better solution
  87. */
  88. public static PreviewImageFragment newInstance(OCFile imageFile, boolean ignoreFirstSavedState){
  89. PreviewImageFragment frag = new PreviewImageFragment();
  90. Bundle args = new Bundle();
  91. args.putParcelable(ARG_FILE, imageFile);
  92. args.putBoolean(ARG_IGNORE_FIRST, ignoreFirstSavedState);
  93. frag.setArguments(args);
  94. return frag;
  95. }
  96. /**
  97. * Creates an empty fragment for image previews.
  98. *
  99. * MUST BE KEPT: the system uses it when tries to reinstantiate a fragment automatically
  100. * (for instance, when the device is turned a aside).
  101. *
  102. * DO NOT CALL IT: an {@link OCFile} and {@link Account} must be provided for a successful
  103. * construction
  104. */
  105. public PreviewImageFragment() {
  106. mIgnoreFirstSavedState = false;
  107. }
  108. /**
  109. * {@inheritDoc}
  110. */
  111. @Override
  112. public void onCreate(Bundle savedInstanceState) {
  113. super.onCreate(savedInstanceState);
  114. Bundle args = getArguments();
  115. setFile((OCFile)args.getParcelable(ARG_FILE));
  116. // TODO better in super, but needs to check ALL the class extending FileFragment;
  117. // not right now
  118. mIgnoreFirstSavedState = args.getBoolean(ARG_IGNORE_FIRST);
  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. View view = inflater.inflate(R.layout.preview_image_fragment, container, false);
  129. mImageView = (TouchImageViewCustom) view.findViewById(R.id.image);
  130. mImageView.setVisibility(View.GONE);
  131. mImageView.setOnClickListener(new OnClickListener() {
  132. @Override
  133. public void onClick(View v) {
  134. ((PreviewImageActivity) getActivity()).toggleFullScreen();
  135. }
  136. });
  137. mMessageView = (TextView)view.findViewById(R.id.message);
  138. mMessageView.setVisibility(View.GONE);
  139. mProgressWheel = (ProgressBar)view.findViewById(R.id.progressWheel);
  140. mProgressWheel.setVisibility(View.VISIBLE);
  141. return view;
  142. }
  143. /**
  144. * {@inheritDoc}
  145. */
  146. @Override
  147. public void onActivityCreated(Bundle savedInstanceState) {
  148. super.onActivityCreated(savedInstanceState);
  149. if (savedInstanceState != null) {
  150. if (!mIgnoreFirstSavedState) {
  151. OCFile file = savedInstanceState.getParcelable(PreviewImageFragment.EXTRA_FILE);
  152. setFile(file);
  153. } else {
  154. mIgnoreFirstSavedState = false;
  155. }
  156. }
  157. if (getFile() == null) {
  158. throw new IllegalStateException("Instanced with a NULL OCFile");
  159. }
  160. if (!getFile().isDown()) {
  161. throw new IllegalStateException("There is no local file to preview");
  162. }
  163. }
  164. /**
  165. * {@inheritDoc}
  166. */
  167. @Override
  168. public void onSaveInstanceState(Bundle outState) {
  169. super.onSaveInstanceState(outState);
  170. outState.putParcelable(PreviewImageFragment.EXTRA_FILE, getFile());
  171. }
  172. @Override
  173. public void onStart() {
  174. super.onStart();
  175. if (getFile() != null) {
  176. mLoadBitmapTask = new LoadBitmapTask(mImageView, mMessageView, mProgressWheel);
  177. //mLoadBitmapTask.execute(new String[]{getFile().getStoragePath()});
  178. // mLoadBitmapTask.execute(getFile().getStoragePath());
  179. mLoadBitmapTask.execute(getFile());
  180. }
  181. }
  182. @Override
  183. public void onStop() {
  184. Log_OC.d(TAG, "onStop starts");
  185. if (mLoadBitmapTask != null) {
  186. mLoadBitmapTask.cancel(true);
  187. mLoadBitmapTask = null;
  188. }
  189. super.onStop();
  190. }
  191. /**
  192. * {@inheritDoc}
  193. */
  194. @Override
  195. public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  196. super.onCreateOptionsMenu(menu, inflater);
  197. inflater.inflate(R.menu.file_actions_menu, menu);
  198. }
  199. /**
  200. * {@inheritDoc}
  201. */
  202. @Override
  203. public void onPrepareOptionsMenu(Menu menu) {
  204. super.onPrepareOptionsMenu(menu);
  205. if (mContainerActivity.getStorageManager() != null && getFile() != null) {
  206. // Update the file
  207. setFile(mContainerActivity.getStorageManager().getFileById(getFile().getFileId()));
  208. FileMenuFilter mf = new FileMenuFilter(
  209. getFile(),
  210. mContainerActivity.getStorageManager().getAccount(),
  211. mContainerActivity,
  212. getActivity()
  213. );
  214. mf.filter(menu);
  215. }
  216. // additional restriction for this fragment
  217. // TODO allow renaming in PreviewImageFragment
  218. MenuItem item = menu.findItem(R.id.action_rename_file);
  219. if (item != null) {
  220. item.setVisible(false);
  221. item.setEnabled(false);
  222. }
  223. // additional restriction for this fragment
  224. // TODO allow refresh file in PreviewImageFragment
  225. item = menu.findItem(R.id.action_sync_file);
  226. if (item != null) {
  227. item.setVisible(false);
  228. item.setEnabled(false);
  229. }
  230. // additional restriction for this fragment
  231. item = menu.findItem(R.id.action_move);
  232. if (item != null) {
  233. item.setVisible(false);
  234. item.setEnabled(false);
  235. }
  236. // additional restriction for this fragment
  237. item = menu.findItem(R.id.action_copy);
  238. if (item != null) {
  239. item.setVisible(false);
  240. item.setEnabled(false);
  241. }
  242. // additional restriction for this fragment
  243. item = menu.findItem(R.id.action_favorite);
  244. if (item != null) {
  245. item.setVisible(false);
  246. item.setEnabled(false);
  247. }
  248. // additional restriction for this fragment
  249. item = menu.findItem(R.id.action_unset_favorite);
  250. if (item != null) {
  251. item.setVisible(false);
  252. item.setEnabled(false);
  253. }
  254. }
  255. /**
  256. * {@inheritDoc}
  257. */
  258. @Override
  259. public boolean onOptionsItemSelected(MenuItem item) {
  260. switch (item.getItemId()) {
  261. case R.id.action_share_file: {
  262. mContainerActivity.getFileOperationsHelper().showShareFile(getFile());
  263. return true;
  264. }
  265. case R.id.action_open_file_with: {
  266. openFile();
  267. return true;
  268. }
  269. case R.id.action_remove_file: {
  270. RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(getFile());
  271. dialog.show(getFragmentManager(), ConfirmationDialogFragment.FTAG_CONFIRMATION);
  272. return true;
  273. }
  274. case R.id.action_see_details: {
  275. seeDetails();
  276. return true;
  277. }
  278. case R.id.action_send_file: {
  279. mContainerActivity.getFileOperationsHelper().sendDownloadedFile(getFile());
  280. return true;
  281. }
  282. case R.id.action_sync_file: {
  283. mContainerActivity.getFileOperationsHelper().syncFile(getFile());
  284. return true;
  285. }
  286. default:
  287. return super.onOptionsItemSelected(item);
  288. }
  289. }
  290. private void seeDetails() {
  291. mContainerActivity.showDetails(getFile());
  292. }
  293. @Override
  294. public void onPause() {
  295. super.onPause();
  296. }
  297. @SuppressFBWarnings("Dm")
  298. @Override
  299. public void onDestroy() {
  300. if (mBitmap != null) {
  301. mBitmap.recycle();
  302. System.gc();
  303. // putting this in onStop() is just the same; the fragment is always destroyed by
  304. // {@link FragmentStatePagerAdapter} when the fragment in swiped further than the
  305. // valid offscreen distance, and onStop() is never called before than that
  306. }
  307. super.onDestroy();
  308. }
  309. /**
  310. * Opens the previewed image with an external application.
  311. */
  312. private void openFile() {
  313. mContainerActivity.getFileOperationsHelper().openFile(getFile());
  314. finish();
  315. }
  316. private class LoadBitmapTask extends AsyncTask<OCFile, Void, LoadImage> {
  317. /**
  318. * Weak reference to the target {@link ImageView} where the bitmap will be loaded into.
  319. *
  320. * Using a weak reference will avoid memory leaks if the target ImageView is retired from
  321. * memory before the load finishes.
  322. */
  323. private final WeakReference<ImageViewCustom> mImageViewRef;
  324. /**
  325. * Weak reference to the target {@link TextView} where error messages will be written.
  326. *
  327. * Using a weak reference will avoid memory leaks if the target ImageView is retired from
  328. * memory before the load finishes.
  329. */
  330. private final WeakReference<TextView> mMessageViewRef;
  331. /**
  332. * Weak reference to the target {@link ProgressBar} shown while the load is in progress.
  333. *
  334. * Using a weak reference will avoid memory leaks if the target ImageView is retired from
  335. * memory before the load finishes.
  336. */
  337. private final WeakReference<ProgressBar> mProgressWheelRef;
  338. /**
  339. * Error message to show when a load fails
  340. */
  341. private int mErrorMessageId;
  342. /**
  343. * Constructor.
  344. *
  345. * @param imageView Target {@link ImageView} where the bitmap will be loaded into.
  346. */
  347. public LoadBitmapTask(ImageViewCustom imageView, TextView messageView,
  348. ProgressBar progressWheel) {
  349. mImageViewRef = new WeakReference<ImageViewCustom>(imageView);
  350. mMessageViewRef = new WeakReference<TextView>(messageView);
  351. mProgressWheelRef = new WeakReference<ProgressBar>(progressWheel);
  352. }
  353. @Override
  354. protected LoadImage doInBackground(OCFile... params) {
  355. Bitmap result = null;
  356. if (params.length != 1) {
  357. return null;
  358. }
  359. OCFile ocFile = params[0];
  360. String storagePath = ocFile.getStoragePath();
  361. try {
  362. int maxDownScale = 3; // could be a parameter passed to doInBackground(...)
  363. Point screenSize = DisplayUtils.getScreenSize(getActivity());
  364. int minWidth = screenSize.x;
  365. int minHeight = screenSize.y;
  366. for (int i = 0; i < maxDownScale && result == null; i++) {
  367. if (isCancelled()) {
  368. return null;
  369. }
  370. try {
  371. result = BitmapUtils.decodeSampledBitmapFromFile(storagePath, minWidth,
  372. minHeight);
  373. if (isCancelled()) {
  374. return new LoadImage(result, ocFile);
  375. }
  376. if (result == null) {
  377. mErrorMessageId = R.string.preview_image_error_unknown_format;
  378. Log_OC.e(TAG, "File could not be loaded as a bitmap: " + storagePath);
  379. break;
  380. } else {
  381. // Rotate image, obeying exif tag.
  382. result = BitmapUtils.rotateImage(result, storagePath);
  383. }
  384. } catch (OutOfMemoryError e) {
  385. mErrorMessageId = R.string.common_error_out_memory;
  386. if (i < maxDownScale - 1) {
  387. Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
  388. " ; scaling down");
  389. minWidth = minWidth / 2;
  390. minHeight = minHeight / 2;
  391. } else {
  392. Log_OC.w(TAG, "Out of memory rendering file " + storagePath +
  393. " ; failing");
  394. }
  395. if (result != null) {
  396. result.recycle();
  397. }
  398. result = null;
  399. }
  400. }
  401. } catch (NoSuchFieldError e) {
  402. mErrorMessageId = R.string.common_error_unknown;
  403. Log_OC.e(TAG, "Error from access to unexisting field despite protection; file "
  404. + storagePath, e);
  405. } catch (Throwable t) {
  406. mErrorMessageId = R.string.common_error_unknown;
  407. Log_OC.e(TAG, "Unexpected error loading " + getFile().getStoragePath(), t);
  408. }
  409. return new LoadImage(result, ocFile);
  410. }
  411. @Override
  412. protected void onCancelled(LoadImage result) {
  413. if (result != null && result.bitmap != null) {
  414. result.bitmap.recycle();
  415. }
  416. }
  417. @Override
  418. protected void onPostExecute(LoadImage result) {
  419. hideProgressWheel();
  420. if (result.bitmap != null) {
  421. showLoadedImage(result);
  422. }
  423. else {
  424. showErrorMessage();
  425. }
  426. if (result.bitmap != null && mBitmap != result.bitmap) {
  427. // unused bitmap, release it! (just in case)
  428. result.bitmap.recycle();
  429. }
  430. }
  431. @SuppressLint("InlinedApi")
  432. private void showLoadedImage(LoadImage result) {
  433. final ImageViewCustom imageView = mImageViewRef.get();
  434. Bitmap bitmap = result.bitmap;
  435. if (imageView != null) {
  436. Log_OC.d(TAG, "Showing image with resolution " + bitmap.getWidth() + "x" +
  437. bitmap.getHeight());
  438. if (result.ocFile.getMimetype().equalsIgnoreCase("image/png")) {
  439. Drawable backrepeat = getResources().getDrawable(R.drawable.backrepeat);
  440. imageView.setBackground(backrepeat);
  441. }
  442. if (result.ocFile.getMimetype().equalsIgnoreCase("image/gif")) {
  443. imageView.setGIFImageFromStoragePath(result.ocFile.getStoragePath());
  444. } else {
  445. imageView.setImageBitmap(bitmap);
  446. }
  447. imageView.setVisibility(View.VISIBLE);
  448. mBitmap = bitmap; // needs to be kept for recycling when not useful
  449. }
  450. final TextView messageView = mMessageViewRef.get();
  451. if (messageView != null) {
  452. messageView.setVisibility(View.GONE);
  453. } // else , silently finish, the fragment was destroyed
  454. }
  455. private void showErrorMessage() {
  456. final ImageView imageView = mImageViewRef.get();
  457. if (imageView != null) {
  458. // shows the default error icon
  459. imageView.setVisibility(View.VISIBLE);
  460. } // else , silently finish, the fragment was destroyed
  461. final TextView messageView = mMessageViewRef.get();
  462. if (messageView != null) {
  463. messageView.setText(mErrorMessageId);
  464. messageView.setVisibility(View.VISIBLE);
  465. } // else , silently finish, the fragment was destroyed
  466. }
  467. private void hideProgressWheel() {
  468. final ProgressBar progressWheel = mProgressWheelRef.get();
  469. if (progressWheel != null) {
  470. progressWheel.setVisibility(View.GONE);
  471. }
  472. }
  473. }
  474. /**
  475. * Helper method to test if an {@link OCFile} can be passed to a {@link PreviewImageFragment}
  476. * to be previewed.
  477. *
  478. * @param file File to test if can be previewed.
  479. * @return 'True' if the file can be handled by the fragment.
  480. */
  481. public static boolean canBePreviewed(OCFile file) {
  482. return (file != null && MimeTypeUtil.isImage(file));
  483. }
  484. /**
  485. * Finishes the preview
  486. */
  487. private void finish() {
  488. Activity container = getActivity();
  489. container.finish();
  490. }
  491. public TouchImageViewCustom getImageView() {
  492. return mImageView;
  493. }
  494. private class LoadImage {
  495. private Bitmap bitmap;
  496. private OCFile ocFile;
  497. public LoadImage(Bitmap bitmap, OCFile ocFile){
  498. this.bitmap = bitmap;
  499. this.ocFile = ocFile;
  500. }
  501. }
  502. }