PreviewImageFragment.java 19 KB

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