FileDetailActivitiesFragment.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Andy Scherzinger
  5. * @author Chris Narkiewicz
  6. *
  7. * Copyright (C) 2018 Andy Scherzinger
  8. * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
  9. *
  10. * This program is free software; you can redistribute it and/or
  11. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  12. * License as published by the Free Software Foundation; either
  13. * version 3 of the License, or any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public
  21. * License along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. */
  23. package com.owncloud.android.ui.fragment;
  24. import android.accounts.Account;
  25. import android.accounts.AccountManager;
  26. import android.accounts.AuthenticatorException;
  27. import android.accounts.OperationCanceledException;
  28. import android.content.Context;
  29. import android.graphics.PorterDuff;
  30. import android.os.AsyncTask;
  31. import android.os.Bundle;
  32. import android.text.Editable;
  33. import android.view.LayoutInflater;
  34. import android.view.View;
  35. import android.view.ViewGroup;
  36. import android.widget.ImageView;
  37. import android.widget.LinearLayout;
  38. import android.widget.ProgressBar;
  39. import android.widget.TextView;
  40. import com.google.android.material.snackbar.Snackbar;
  41. import com.google.android.material.textfield.TextInputEditText;
  42. import com.nextcloud.client.account.CurrentAccountProvider;
  43. import com.nextcloud.client.di.Injectable;
  44. import com.owncloud.android.MainApp;
  45. import com.owncloud.android.R;
  46. import com.owncloud.android.authentication.AccountUtils;
  47. import com.owncloud.android.datamodel.FileDataStorageManager;
  48. import com.owncloud.android.datamodel.OCFile;
  49. import com.owncloud.android.lib.common.OwnCloudAccount;
  50. import com.owncloud.android.lib.common.OwnCloudClient;
  51. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  52. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  53. import com.owncloud.android.lib.common.utils.Log_OC;
  54. import com.owncloud.android.lib.resources.activities.GetActivitiesRemoteOperation;
  55. import com.owncloud.android.lib.resources.activities.model.RichObject;
  56. import com.owncloud.android.lib.resources.comments.MarkCommentsAsReadRemoteOperation;
  57. import com.owncloud.android.lib.resources.files.ReadFileVersionsRemoteOperation;
  58. import com.owncloud.android.lib.resources.files.model.FileVersion;
  59. import com.owncloud.android.lib.resources.status.OCCapability;
  60. import com.owncloud.android.lib.resources.status.OwnCloudVersion;
  61. import com.owncloud.android.operations.CommentFileOperation;
  62. import com.owncloud.android.ui.activity.ComponentsGetter;
  63. import com.owncloud.android.ui.activity.FileActivity;
  64. import com.owncloud.android.ui.adapter.ActivityAndVersionListAdapter;
  65. import com.owncloud.android.ui.events.CommentsEvent;
  66. import com.owncloud.android.ui.helpers.FileOperationsHelper;
  67. import com.owncloud.android.ui.interfaces.ActivityListInterface;
  68. import com.owncloud.android.ui.interfaces.VersionListInterface;
  69. import com.owncloud.android.utils.ThemeUtils;
  70. import org.apache.commons.httpclient.HttpStatus;
  71. import org.greenrobot.eventbus.EventBus;
  72. import java.io.IOException;
  73. import java.util.ArrayList;
  74. import java.util.List;
  75. import javax.inject.Inject;
  76. import androidx.annotation.NonNull;
  77. import androidx.fragment.app.Fragment;
  78. import androidx.fragment.app.FragmentActivity;
  79. import androidx.recyclerview.widget.LinearLayoutManager;
  80. import androidx.recyclerview.widget.RecyclerView;
  81. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  82. import butterknife.BindString;
  83. import butterknife.BindView;
  84. import butterknife.ButterKnife;
  85. import butterknife.OnClick;
  86. import butterknife.Unbinder;
  87. public class FileDetailActivitiesFragment extends Fragment implements
  88. ActivityListInterface,
  89. VersionListInterface.View,
  90. Injectable {
  91. private static final String TAG = FileDetailActivitiesFragment.class.getSimpleName();
  92. private static final String ARG_FILE = "FILE";
  93. private static final String ARG_ACCOUNT = "ACCOUNT";
  94. private ActivityAndVersionListAdapter adapter;
  95. private Unbinder unbinder;
  96. private OwnCloudClient ownCloudClient;
  97. private OCFile file;
  98. private Account account;
  99. private String nextPageUrl;
  100. private boolean isLoadingActivities;
  101. @BindView(R.id.empty_list_view)
  102. public LinearLayout emptyContentContainer;
  103. @BindView(R.id.swipe_containing_list)
  104. public SwipeRefreshLayout swipeListRefreshLayout;
  105. @BindView(R.id.swipe_containing_empty)
  106. public SwipeRefreshLayout swipeEmptyListRefreshLayout;
  107. @BindView(R.id.empty_list_view_text)
  108. public TextView emptyContentMessage;
  109. @BindView(R.id.empty_list_view_headline)
  110. public TextView emptyContentHeadline;
  111. @BindView(R.id.empty_list_icon)
  112. public ImageView emptyContentIcon;
  113. @BindView(R.id.empty_list_progress)
  114. public ProgressBar emptyContentProgressBar;
  115. @BindView(android.R.id.list)
  116. public RecyclerView recyclerView;
  117. @BindView(R.id.commentInputField)
  118. public TextInputEditText commentInput;
  119. @BindString(R.string.activities_no_results_headline)
  120. public String noResultsHeadline;
  121. @BindString(R.string.activities_no_results_message)
  122. public String noResultsMessage;
  123. private boolean restoreFileVersionSupported;
  124. private String userId;
  125. private FileOperationsHelper operationsHelper;
  126. private VersionListInterface.CommentCallback callback;
  127. @Inject
  128. protected CurrentAccountProvider accountManager;
  129. public static FileDetailActivitiesFragment newInstance(OCFile file, Account account) {
  130. FileDetailActivitiesFragment fragment = new FileDetailActivitiesFragment();
  131. Bundle args = new Bundle();
  132. args.putParcelable(ARG_FILE, file);
  133. args.putParcelable(ARG_ACCOUNT, account);
  134. fragment.setArguments(args);
  135. return fragment;
  136. }
  137. @Override
  138. public View onCreateView(@NonNull LayoutInflater inflater,
  139. ViewGroup container,
  140. Bundle savedInstanceState) {
  141. file = getArguments().getParcelable(ARG_FILE);
  142. account = getArguments().getParcelable(ARG_ACCOUNT);
  143. if (savedInstanceState != null) {
  144. file = savedInstanceState.getParcelable(FileActivity.EXTRA_FILE);
  145. account = savedInstanceState.getParcelable(FileActivity.EXTRA_ACCOUNT);
  146. }
  147. View view = inflater.inflate(R.layout.file_details_activities_fragment, container, false);
  148. unbinder = ButterKnife.bind(this, view);
  149. setupView();
  150. onCreateSwipeToRefresh(swipeEmptyListRefreshLayout);
  151. onCreateSwipeToRefresh(swipeListRefreshLayout);
  152. fetchAndSetData(null);
  153. swipeListRefreshLayout.setOnRefreshListener(() -> onRefreshListLayout(swipeListRefreshLayout));
  154. swipeEmptyListRefreshLayout.setOnRefreshListener(() -> onRefreshListLayout(swipeEmptyListRefreshLayout));
  155. AccountManager accountManager = AccountManager.get(getContext());
  156. userId = accountManager.getUserData(account,
  157. com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
  158. callback = new VersionListInterface.CommentCallback() {
  159. @Override
  160. public void onSuccess() {
  161. commentInput.getText().clear();
  162. fetchAndSetData(null);
  163. }
  164. @Override
  165. public void onError(int error) {
  166. Snackbar.make(recyclerView, error, Snackbar.LENGTH_LONG).show();
  167. }
  168. };
  169. commentInput.getBackground().setColorFilter(
  170. ThemeUtils.primaryAccentColor(getContext()),
  171. PorterDuff.Mode.SRC_ATOP
  172. );
  173. ThemeUtils.themeEditText(getContext(), commentInput, false);
  174. return view;
  175. }
  176. @OnClick(R.id.submitComment)
  177. public void submitComment() {
  178. Editable commentField = commentInput.getText();
  179. if (commentField == null) {
  180. return;
  181. }
  182. String trimmedComment = commentField.toString().trim();
  183. if (trimmedComment.length() > 0) {
  184. new SubmitCommentTask(trimmedComment, userId, file.getLocalId(), callback, ownCloudClient).execute();
  185. }
  186. }
  187. private void onRefreshListLayout(SwipeRefreshLayout refreshLayout) {
  188. setLoadingMessage();
  189. if (refreshLayout != null && refreshLayout.isRefreshing()) {
  190. refreshLayout.setRefreshing(false);
  191. }
  192. fetchAndSetData(null);
  193. }
  194. private void setLoadingMessage() {
  195. emptyContentHeadline.setText(R.string.file_list_loading);
  196. emptyContentMessage.setText("");
  197. emptyContentIcon.setVisibility(View.GONE);
  198. emptyContentProgressBar.setVisibility(View.VISIBLE);
  199. }
  200. @Override
  201. public void onDestroy() {
  202. super.onDestroy();
  203. unbinder.unbind();
  204. }
  205. private void setupView() {
  206. FileDataStorageManager storageManager = new FileDataStorageManager(account, requireActivity().getContentResolver());
  207. operationsHelper = ((ComponentsGetter) requireActivity()).getFileOperationsHelper();
  208. OCCapability capability = storageManager.getCapability(account.name);
  209. OwnCloudVersion serverVersion = AccountUtils.getServerVersion(account);
  210. restoreFileVersionSupported = capability.getFilesVersioning().isTrue() &&
  211. serverVersion.compareTo(OwnCloudVersion.nextcloud_14) >= 0;
  212. emptyContentProgressBar.getIndeterminateDrawable().setColorFilter(ThemeUtils.primaryAccentColor(getContext()),
  213. PorterDuff.Mode.SRC_IN);
  214. emptyContentIcon.setImageDrawable(getResources().getDrawable(R.drawable.ic_activity_light_grey));
  215. adapter = new ActivityAndVersionListAdapter(getContext(), accountManager, this, this, storageManager, capability);
  216. recyclerView.setAdapter(adapter);
  217. LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
  218. recyclerView.setLayoutManager(layoutManager);
  219. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  220. @Override
  221. public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
  222. super.onScrolled(recyclerView, dx, dy);
  223. int visibleItemCount = recyclerView.getChildCount();
  224. int totalItemCount = layoutManager.getItemCount();
  225. int firstVisibleItemIndex = layoutManager.findFirstVisibleItemPosition();
  226. // synchronize loading state when item count changes
  227. if (!isLoadingActivities && (totalItemCount - visibleItemCount) <= (firstVisibleItemIndex + 5)
  228. && nextPageUrl != null && !nextPageUrl.isEmpty()) {
  229. // Almost reached the end, continue to load new activities
  230. fetchAndSetData(nextPageUrl);
  231. }
  232. }
  233. });
  234. }
  235. public void reload() {
  236. fetchAndSetData(null);
  237. }
  238. /**
  239. * @param pageUrl String
  240. */
  241. private void fetchAndSetData(String pageUrl) {
  242. final Account currentAccount = accountManager.getCurrentAccount();
  243. final Context context = MainApp.getAppContext();
  244. final FragmentActivity activity = getActivity();
  245. final SwipeRefreshLayout empty = swipeEmptyListRefreshLayout;
  246. final SwipeRefreshLayout list = swipeListRefreshLayout;
  247. Thread t = new Thread(() -> {
  248. OwnCloudAccount ocAccount;
  249. try {
  250. ocAccount = new OwnCloudAccount(currentAccount, context);
  251. ownCloudClient = OwnCloudClientManagerFactory.getDefaultSingleton().
  252. getClientFor(ocAccount, MainApp.getAppContext());
  253. ownCloudClient.setOwnCloudVersion(AccountUtils.getServerVersion(currentAccount));
  254. isLoadingActivities = true;
  255. GetActivitiesRemoteOperation getRemoteNotificationOperation = new GetActivitiesRemoteOperation(
  256. file.getLocalId());
  257. if (pageUrl != null) {
  258. getRemoteNotificationOperation.setNextUrl(pageUrl);
  259. }
  260. Log_OC.d(TAG, "BEFORE getRemoteActivitiesOperation.execute");
  261. final RemoteOperationResult result = getRemoteNotificationOperation.execute(ownCloudClient);
  262. ArrayList<Object> versions = null;
  263. if (restoreFileVersionSupported) {
  264. ReadFileVersionsRemoteOperation readFileVersionsOperation = new ReadFileVersionsRemoteOperation(
  265. file.getLocalId(), userId);
  266. RemoteOperationResult result1 = readFileVersionsOperation.execute(ownCloudClient);
  267. versions = result1.getData();
  268. }
  269. if (result.isSuccess() && result.getData() != null) {
  270. final List<Object> data = result.getData();
  271. final List<Object> activitiesAndVersions = (ArrayList) data.get(0);
  272. if (restoreFileVersionSupported && versions != null) {
  273. activitiesAndVersions.addAll(versions);
  274. }
  275. nextPageUrl = (String) data.get(1);
  276. activity.runOnUiThread(() -> {
  277. populateList(activitiesAndVersions, pageUrl == null);
  278. if (activitiesAndVersions.isEmpty()) {
  279. setEmptyContent(noResultsHeadline, noResultsMessage);
  280. list.setVisibility(View.GONE);
  281. empty.setVisibility(View.VISIBLE);
  282. } else {
  283. empty.setVisibility(View.GONE);
  284. list.setVisibility(View.VISIBLE);
  285. }
  286. isLoadingActivities = false;
  287. });
  288. } else {
  289. Log_OC.d(TAG, result.getLogMessage());
  290. // show error
  291. String logMessage = result.getLogMessage();
  292. if (result.getHttpCode() == HttpStatus.SC_NOT_MODIFIED) {
  293. logMessage = noResultsMessage;
  294. }
  295. final String finalLogMessage = logMessage;
  296. activity.runOnUiThread(() -> {
  297. setErrorContent(finalLogMessage);
  298. isLoadingActivities = false;
  299. });
  300. }
  301. hideRefreshLayoutLoader(activity);
  302. } catch (com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException | IOException |
  303. OperationCanceledException | AuthenticatorException e) {
  304. Log_OC.e(TAG, "Error fetching file details activities", e);
  305. }
  306. });
  307. t.start();
  308. }
  309. public void markCommentsAsRead() {
  310. new Thread(() -> {
  311. if (file.getUnreadCommentsCount() > 0) {
  312. MarkCommentsAsReadRemoteOperation unreadOperation = new MarkCommentsAsReadRemoteOperation(
  313. file.getLocalId());
  314. RemoteOperationResult remoteOperationResult = unreadOperation.execute(ownCloudClient);
  315. if (remoteOperationResult.isSuccess()) {
  316. EventBus.getDefault().post(new CommentsEvent(file.getRemoteId()));
  317. }
  318. }
  319. }).start();
  320. }
  321. private void populateList(List<Object> activities, boolean clear) {
  322. adapter.setActivityAndVersionItems(activities, ownCloudClient, clear);
  323. }
  324. private void setEmptyContent(String headline, String message) {
  325. if (emptyContentContainer != null && emptyContentMessage != null) {
  326. emptyContentIcon.setImageDrawable(requireContext().getResources().getDrawable(R.drawable.ic_activity_light_grey));
  327. emptyContentHeadline.setText(headline);
  328. emptyContentMessage.setText(message);
  329. emptyContentMessage.setVisibility(View.VISIBLE);
  330. emptyContentProgressBar.setVisibility(View.GONE);
  331. emptyContentIcon.setVisibility(View.VISIBLE);
  332. }
  333. }
  334. private void setErrorContent(String message) {
  335. if (emptyContentContainer != null && emptyContentMessage != null) {
  336. emptyContentHeadline.setText(R.string.common_error);
  337. emptyContentIcon.setImageDrawable(requireContext().getResources().getDrawable(R.drawable.ic_list_empty_error));
  338. emptyContentMessage.setText(message);
  339. emptyContentMessage.setVisibility(View.VISIBLE);
  340. emptyContentProgressBar.setVisibility(View.GONE);
  341. emptyContentIcon.setVisibility(View.VISIBLE);
  342. }
  343. }
  344. private void hideRefreshLayoutLoader(FragmentActivity activity) {
  345. activity.runOnUiThread(() -> {
  346. if (swipeListRefreshLayout != null) {
  347. swipeListRefreshLayout.setRefreshing(false);
  348. }
  349. if (swipeEmptyListRefreshLayout != null) {
  350. swipeEmptyListRefreshLayout.setRefreshing(false);
  351. }
  352. isLoadingActivities = false;
  353. });
  354. }
  355. protected void onCreateSwipeToRefresh(SwipeRefreshLayout refreshLayout) {
  356. int primaryColor = ThemeUtils.primaryColor(getContext());
  357. int darkColor = ThemeUtils.primaryDarkColor(getContext());
  358. int accentColor = ThemeUtils.primaryAccentColor(getContext());
  359. // Colors in animations
  360. refreshLayout.setColorSchemeColors(accentColor, primaryColor, darkColor);
  361. }
  362. @Override
  363. public void onActivityClicked(RichObject richObject) {
  364. // TODO implement activity click
  365. }
  366. @Override
  367. public void onSaveInstanceState(@NonNull Bundle outState) {
  368. super.onSaveInstanceState(outState);
  369. outState.putParcelable(FileActivity.EXTRA_FILE, file);
  370. outState.putParcelable(FileActivity.EXTRA_ACCOUNT, account);
  371. }
  372. @Override
  373. public void onRestoreClicked(FileVersion fileVersion) {
  374. operationsHelper.restoreFileVersion(fileVersion, userId);
  375. }
  376. private static class SubmitCommentTask extends AsyncTask<Void, Void, Boolean> {
  377. private String message;
  378. private String userId;
  379. private String fileId;
  380. private VersionListInterface.CommentCallback callback;
  381. private OwnCloudClient client;
  382. private SubmitCommentTask(String message, String userId, String fileId,
  383. VersionListInterface.CommentCallback callback, OwnCloudClient client) {
  384. this.message = message;
  385. this.userId = userId;
  386. this.fileId = fileId;
  387. this.callback = callback;
  388. this.client = client;
  389. }
  390. @Override
  391. protected Boolean doInBackground(Void... voids) {
  392. CommentFileOperation commentFileOperation = new CommentFileOperation(message, fileId, userId);
  393. RemoteOperationResult result = commentFileOperation.execute(client);
  394. return result.isSuccess();
  395. }
  396. @Override
  397. protected void onPostExecute(Boolean success) {
  398. super.onPostExecute(success);
  399. if (success) {
  400. callback.onSuccess();
  401. } else {
  402. callback.onError(R.string.error_comment_file);
  403. }
  404. }
  405. }
  406. }