FileDetailSharingFragment.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Andy Scherzinger
  5. * @author Chris Narkiewicz <hello@ezaquarii.com>
  6. * @author TSI-mc
  7. *
  8. * Copyright (C) 2018 Andy Scherzinger
  9. * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
  10. * Copyright (C) 2020 TSI-mc
  11. *
  12. * This program is free software; you can redistribute it and/or
  13. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  14. * License as published by the Free Software Foundation; either
  15. * version 3 of the License, or any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public
  23. * License along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. */
  25. package com.owncloud.android.ui.fragment;
  26. import android.accounts.AccountManager;
  27. import android.app.AlertDialog;
  28. import android.app.SearchManager;
  29. import android.content.Context;
  30. import android.graphics.drawable.Drawable;
  31. import android.os.Bundle;
  32. import android.text.InputType;
  33. import android.text.TextUtils;
  34. import android.view.LayoutInflater;
  35. import android.view.View;
  36. import android.view.ViewGroup;
  37. import com.nextcloud.client.account.User;
  38. import com.nextcloud.client.account.UserAccountManager;
  39. import com.nextcloud.client.di.Injectable;
  40. import com.nextcloud.client.network.ClientFactory;
  41. import com.owncloud.android.R;
  42. import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding;
  43. import com.owncloud.android.datamodel.FileDataStorageManager;
  44. import com.owncloud.android.datamodel.OCFile;
  45. import com.owncloud.android.lib.common.OwnCloudAccount;
  46. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  47. import com.owncloud.android.lib.resources.shares.OCShare;
  48. import com.owncloud.android.lib.resources.shares.ShareType;
  49. import com.owncloud.android.lib.resources.status.NextcloudVersion;
  50. import com.owncloud.android.lib.resources.status.OCCapability;
  51. import com.owncloud.android.lib.resources.status.OwnCloudVersion;
  52. import com.owncloud.android.ui.activity.FileActivity;
  53. import com.owncloud.android.ui.activity.FileDisplayActivity;
  54. import com.owncloud.android.ui.adapter.ShareeListAdapter;
  55. import com.owncloud.android.ui.adapter.ShareeListAdapterListener;
  56. import com.owncloud.android.ui.asynctasks.RetrieveHoverCardAsyncTask;
  57. import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
  58. import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper;
  59. import com.owncloud.android.ui.helpers.FileOperationsHelper;
  60. import com.owncloud.android.utils.ClipboardUtil;
  61. import com.owncloud.android.utils.DisplayUtils;
  62. import com.owncloud.android.utils.theme.ThemeToolbarUtils;
  63. import java.util.ArrayList;
  64. import java.util.List;
  65. import javax.inject.Inject;
  66. import androidx.annotation.NonNull;
  67. import androidx.annotation.Nullable;
  68. import androidx.annotation.VisibleForTesting;
  69. import androidx.appcompat.widget.SearchView;
  70. import androidx.fragment.app.Fragment;
  71. import androidx.recyclerview.widget.LinearLayoutManager;
  72. public class FileDetailSharingFragment extends Fragment implements ShareeListAdapterListener,
  73. DisplayUtils.AvatarGenerationListener,
  74. Injectable, FileDetailsSharingMenuBottomSheetActions, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions {
  75. private static final String ARG_FILE = "FILE";
  76. private static final String ARG_USER = "USER";
  77. public static final int PERMISSION_EDITING_ALLOWED = 17;
  78. private OCFile file;
  79. private User user;
  80. private OCCapability capabilities;
  81. private FileOperationsHelper fileOperationsHelper;
  82. private FileActivity fileActivity;
  83. private FileDataStorageManager fileDataStorageManager;
  84. private FileDetailsSharingFragmentBinding binding;
  85. private OnEditShareListener onEditShareListener;
  86. @Inject UserAccountManager accountManager;
  87. @Inject ClientFactory clientFactory;
  88. public static FileDetailSharingFragment newInstance(OCFile file, User user) {
  89. FileDetailSharingFragment fragment = new FileDetailSharingFragment();
  90. Bundle args = new Bundle();
  91. args.putParcelable(ARG_FILE, file);
  92. args.putParcelable(ARG_USER, user);
  93. fragment.setArguments(args);
  94. return fragment;
  95. }
  96. @Override
  97. public void onCreate(@Nullable Bundle savedInstanceState) {
  98. super.onCreate(savedInstanceState);
  99. if (savedInstanceState != null) {
  100. file = savedInstanceState.getParcelable(ARG_FILE);
  101. user = savedInstanceState.getParcelable(ARG_USER);
  102. } else {
  103. Bundle arguments = getArguments();
  104. if (arguments != null) {
  105. file = getArguments().getParcelable(ARG_FILE);
  106. user = getArguments().getParcelable(ARG_USER);
  107. }
  108. }
  109. if (file == null) {
  110. throw new IllegalArgumentException("File may not be null");
  111. }
  112. if (user == null) {
  113. throw new IllegalArgumentException("Account may not be null");
  114. }
  115. fileActivity = (FileActivity) getActivity();
  116. if (fileActivity == null) {
  117. throw new IllegalArgumentException("FileActivity may not be null");
  118. }
  119. }
  120. @Override
  121. public void onActivityCreated(Bundle savedInstanceState) {
  122. super.onActivityCreated(savedInstanceState);
  123. refreshCapabilitiesFromDB();
  124. refreshSharesFromDB();
  125. }
  126. @Override
  127. public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  128. binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false);
  129. View view = binding.getRoot();
  130. fileOperationsHelper = fileActivity.getFileOperationsHelper();
  131. fileDataStorageManager = fileActivity.getStorageManager();
  132. AccountManager accountManager = AccountManager.get(getContext());
  133. String userId = accountManager.getUserData(user.toPlatformAccount(),
  134. com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
  135. binding.sharesList.setAdapter(new ShareeListAdapter(fileActivity,
  136. new ArrayList<>(),
  137. this,
  138. userId,
  139. user));
  140. binding.sharesList.setLayoutManager(new LinearLayoutManager(getContext()));
  141. setupView();
  142. return view;
  143. }
  144. @Override
  145. public void onDestroyView() {
  146. super.onDestroyView();
  147. binding = null;
  148. }
  149. @Override
  150. public void onAttach(@NonNull Context context) {
  151. super.onAttach(context);
  152. if (!(getActivity() instanceof FileActivity)) {
  153. throw new IllegalArgumentException("Calling activity must be of type FileActivity");
  154. }
  155. try {
  156. onEditShareListener = (OnEditShareListener) context;
  157. } catch (Exception ignored) {
  158. throw new IllegalArgumentException("Calling activity must implement the interface");
  159. }
  160. }
  161. private void setupView() {
  162. setShareWithYou();
  163. FileDetailSharingFragmentHelper.setupSearchView(
  164. (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE),
  165. binding.searchView,
  166. fileActivity.getComponentName());
  167. ThemeToolbarUtils.themeSearchView(binding.searchView, requireContext());
  168. if (file.canReshare()) {
  169. binding.searchView.setQueryHint(getResources().getString(R.string.share_search));
  170. } else {
  171. binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed));
  172. binding.searchView.setInputType(InputType.TYPE_NULL);
  173. disableSearchView(binding.searchView);
  174. }
  175. }
  176. private void disableSearchView(View view) {
  177. view.setEnabled(false);
  178. if (view instanceof ViewGroup) {
  179. ViewGroup viewGroup = (ViewGroup) view;
  180. for (int i = 0; i < viewGroup.getChildCount(); i++) {
  181. disableSearchView(viewGroup.getChildAt(i));
  182. }
  183. }
  184. }
  185. private void setShareWithYou() {
  186. if (accountManager.userOwnsFile(file, user)) {
  187. binding.sharedWithYouContainer.setVisibility(View.GONE);
  188. } else {
  189. binding.sharedWithYouUsername.setText(
  190. String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName()));
  191. DisplayUtils.setAvatar(user,
  192. file.getOwnerId(),
  193. this,
  194. getResources().getDimension(
  195. R.dimen.file_list_item_avatar_icon_radius),
  196. getResources(),
  197. binding.sharedWithYouAvatar,
  198. getContext());
  199. binding.sharedWithYouAvatar.setVisibility(View.VISIBLE);
  200. String note = file.getNote();
  201. if (!TextUtils.isEmpty(note)) {
  202. binding.sharedWithYouNote.setText(file.getNote());
  203. binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE);
  204. } else {
  205. binding.sharedWithYouNoteContainer.setVisibility(View.GONE);
  206. }
  207. }
  208. }
  209. @Override
  210. public void copyInternalLink() {
  211. OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount();
  212. if (account == null) {
  213. DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_url));
  214. return;
  215. }
  216. FileDisplayActivity.showShareLinkDialog(fileActivity, file, createInternalLink(account, file));
  217. }
  218. private String createInternalLink(OwnCloudAccount account, OCFile file) {
  219. return account.getBaseUri() + "/index.php/f/" + file.getLocalId();
  220. }
  221. @Override
  222. public void createPublicShareLink() {
  223. if (capabilities != null && (capabilities.getFilesSharingPublicPasswordEnforced().isTrue() ||
  224. capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue())) {
  225. // password enforced by server, request to the user before trying to create
  226. requestPasswordForShareViaLink(true,
  227. capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue());
  228. } else {
  229. // create without password if not enforced by server or we don't know if enforced;
  230. fileOperationsHelper.shareFileViaPublicShare(file, null);
  231. }
  232. }
  233. private void showSendLinkTo(OCShare publicShare) {
  234. if (file.isSharedViaLink()) {
  235. if (TextUtils.isEmpty(publicShare.getShareLink())) {
  236. fileOperationsHelper.getFileWithLink(file);
  237. } else {
  238. FileDisplayActivity.showShareLinkDialog(fileActivity, file, publicShare.getShareLink());
  239. }
  240. }
  241. }
  242. public void copyLink(OCShare share) {
  243. if (file.isSharedViaLink()) {
  244. if (TextUtils.isEmpty(share.getShareLink())) {
  245. fileOperationsHelper.getFileWithLink(file);
  246. } else {
  247. ClipboardUtil.copyToClipboard(getActivity(), share.getShareLink());
  248. }
  249. }
  250. }
  251. /**
  252. * show share action bottom sheet
  253. *
  254. * @param share
  255. */
  256. @Override
  257. @VisibleForTesting
  258. public void showSharingMenuActionSheet(OCShare share) {
  259. new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share).show();
  260. }
  261. /**
  262. * show quick sharing permission dialog
  263. * @param share
  264. */
  265. @Override
  266. public void showPermissionsDialog(OCShare share) {
  267. new QuickSharingPermissionsBottomSheetDialog(fileActivity, this, share).show();
  268. }
  269. /**
  270. * Updates the UI after the result of an update operation on the edited {@link OCFile}.
  271. *
  272. * @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information.
  273. * @param file the edited {@link OCFile}
  274. */
  275. public void onUpdateShareInformation(RemoteOperationResult result, OCFile file) {
  276. this.file = file;
  277. if (result.isSuccess()) {
  278. refreshUiFromDB();
  279. } else {
  280. setupView();
  281. }
  282. }
  283. /**
  284. * Get {@link OCShare} instance from DB and updates the UI.
  285. */
  286. private void refreshUiFromDB() {
  287. refreshSharesFromDB();
  288. // Updates UI with new state
  289. setupView();
  290. }
  291. private void unshareWith(OCShare share) {
  292. fileOperationsHelper.unshareShare(file, share);
  293. }
  294. /**
  295. * Starts a dialog that requests a password to the user to protect a share link.
  296. *
  297. * @param createShare When 'true', the request for password will be followed by the creation of a new public
  298. * link; when 'false', a public share is assumed to exist, and the password is bound to it.
  299. * @param askForPassword if true, password is optional
  300. */
  301. public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) {
  302. SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file,
  303. createShare,
  304. askForPassword);
  305. dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
  306. }
  307. @Override
  308. public void requestPasswordForShare(OCShare share, boolean askForPassword) {
  309. SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword);
  310. dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
  311. }
  312. @Override
  313. public void showProfileBottomSheet(User user, String shareWith) {
  314. if (user.getServer().getVersion().isNewerOrEqual(NextcloudVersion.Companion.getNextcloud_23())) {
  315. new RetrieveHoverCardAsyncTask(user, shareWith, fileActivity, clientFactory).execute();
  316. }
  317. }
  318. /**
  319. * Get known server capabilities from DB
  320. */
  321. public void refreshCapabilitiesFromDB() {
  322. capabilities = fileDataStorageManager.getCapability(user.getAccountName());
  323. }
  324. /**
  325. * Get public link from the DB to fill in the "Share link" section in the UI. Takes into account server capabilities
  326. * before reading database.
  327. */
  328. public void refreshSharesFromDB() {
  329. ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter();
  330. if (adapter == null) {
  331. DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_shares));
  332. return;
  333. }
  334. adapter.getShares().clear();
  335. // to show share with users/groups info
  336. List<OCShare> shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(),
  337. user.toPlatformAccount().name);
  338. adapter.addShares(shares);
  339. if (FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities) || !file.canReshare()) {
  340. return;
  341. }
  342. // Get public share
  343. List<OCShare> publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(),
  344. ShareType.PUBLIC_LINK,
  345. "");
  346. if (publicShares.isEmpty() && containsNoNewPublicShare(adapter.getShares())) {
  347. publicShares.add(new OCShare().setShareType(ShareType.NEW_PUBLIC_LINK));
  348. } else {
  349. adapter.removeNewPublicShare();
  350. }
  351. adapter.addShares(publicShares);
  352. }
  353. private boolean containsNoNewPublicShare(List<OCShare> shares) {
  354. for (OCShare share : shares) {
  355. if (share.getShareType() == ShareType.NEW_PUBLIC_LINK) {
  356. return false;
  357. }
  358. }
  359. return true;
  360. }
  361. @Override
  362. public void onSaveInstanceState(@NonNull Bundle outState) {
  363. super.onSaveInstanceState(outState);
  364. outState.putParcelable(ARG_FILE, file);
  365. outState.putParcelable(ARG_USER, user);
  366. }
  367. @Override
  368. public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
  369. binding.sharedWithYouAvatar.setImageDrawable(avatarDrawable);
  370. }
  371. @Override
  372. public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
  373. return false;
  374. }
  375. private boolean isReshareForbidden(OCShare share) {
  376. return ShareType.FEDERATED.equals(share.getShareType()) ||
  377. capabilities != null && capabilities.getFilesSharingResharing().isFalse();
  378. }
  379. @VisibleForTesting
  380. public void search(String query) {
  381. SearchView searchView = getView().findViewById(R.id.searchView);
  382. searchView.setQuery(query, true);
  383. }
  384. public OCFile getFile() {
  385. return file;
  386. }
  387. @Override
  388. public void openIn(OCShare share) {
  389. fileOperationsHelper.sendShareFile(file);
  390. }
  391. @Override
  392. public void advancedPermissions(OCShare share) {
  393. modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION);
  394. }
  395. @Override
  396. public void sendNewEmail(OCShare share) {
  397. modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE);
  398. }
  399. @Override
  400. public void unShare(OCShare share) {
  401. unshareWith(share);
  402. ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter();
  403. if (adapter == null) {
  404. DisplayUtils.showSnackMessage(getView(), getString(R.string.failed_update_ui));
  405. return;
  406. }
  407. adapter.remove(share);
  408. }
  409. @Override
  410. public void sendLink(OCShare share) {
  411. if (file.isSharedViaLink() && !TextUtils.isEmpty(share.getShareLink())) {
  412. FileDisplayActivity.showShareLinkDialog(fileActivity, file, share.getShareLink());
  413. } else {
  414. showSendLinkTo(share);
  415. }
  416. }
  417. @Override
  418. public void addAnotherLink(OCShare share) {
  419. createPublicShareLink();
  420. }
  421. private void modifyExistingShare(OCShare share, int screenTypePermission) {
  422. onEditShareListener.editExistingShare(share, screenTypePermission, !isReshareForbidden(share),
  423. capabilities.getVersion().isNewerOrEqual(OwnCloudVersion.nextcloud_18));
  424. }
  425. @Override
  426. public void onQuickPermissionChanged(OCShare share, int permission) {
  427. fileOperationsHelper.setPermissionsToShare(share, permission);
  428. }
  429. public interface OnEditShareListener {
  430. void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown,
  431. boolean isExpiryDateShown);
  432. void onShareProcessClosed();
  433. }
  434. }