PreviewImageFragment.java 21 KB

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