/*
* Nextcloud Android client application
*
* @author Andy Scherzinger
* Copyright (C) 2018 Andy Scherzinger
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see .
*/
package com.owncloud.android.ui.fragment;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.SearchManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable;
import com.owncloud.android.R;
import com.owncloud.android.authentication.AccountUtils;
import com.owncloud.android.datamodel.FileDataStorageManager;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.SharePermissionsBuilder;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.lib.resources.status.OCCapability;
import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.activity.FileDisplayActivity;
import com.owncloud.android.ui.adapter.UserListAdapter;
import com.owncloud.android.ui.decoration.SimpleListItemDividerDecoration;
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment;
import com.owncloud.android.ui.dialog.NoteDialogFragment;
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment;
import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper;
import com.owncloud.android.ui.fragment.util.SharingMenuHelper;
import com.owncloud.android.ui.helpers.FileOperationsHelper;
import com.owncloud.android.utils.ClipboardUtil;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.ThemeUtils;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class FileDetailSharingFragment extends Fragment implements UserListAdapter.ShareeListAdapterListener,
DisplayUtils.AvatarGenerationListener, Injectable {
private static final String ARG_FILE = "FILE";
private static final String ARG_ACCOUNT = "ACCOUNT";
// to show share with users/groups info
private List shares;
private OCFile file;
private Account account;
private OCCapability capabilities;
private OCShare publicShare;
private FileOperationsHelper fileOperationsHelper;
private FileDisplayActivity fileDisplayActivity;
private FileDataStorageManager fileDataStorageManager;
private Unbinder unbinder;
@BindView(R.id.searchView)
SearchView searchView;
@BindView(R.id.shareUsersList)
RecyclerView usersList;
@BindView(R.id.shareNoUsers)
TextView noList;
@BindView(R.id.share_by_link)
AppCompatCheckBox shareByLink;
@BindView(R.id.share_link_copy_icon)
ImageView shareLinkCopyIcon;
@BindView(R.id.overflow_menu_share_link)
ImageView overflowMenuShareLink;
@BindView(R.id.share_by_link_allow_editing)
AppCompatCheckBox shareByLinkAllowEditing;
@BindView(R.id.share_by_link_container)
LinearLayout shareByLinkContainer;
@BindView(R.id.shared_with_you_container)
LinearLayout sharedWithYouContainer;
@BindView(R.id.shared_with_you_avatar)
ImageView sharedWithYouAvatar;
@BindView(R.id.shared_with_you_username)
TextView sharedWithYouUsername;
@BindView(R.id.shared_with_you_note)
TextView sharedWithYouNote;
@Inject UserAccountManager accountManager;
public static FileDetailSharingFragment newInstance(OCFile file, Account account) {
FileDetailSharingFragment fragment = new FileDetailSharingFragment();
Bundle args = new Bundle();
args.putParcelable(ARG_FILE, file);
args.putParcelable(ARG_ACCOUNT, account);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
file = savedInstanceState.getParcelable(FileActivity.EXTRA_FILE);
account = savedInstanceState.getParcelable(FileActivity.EXTRA_ACCOUNT);
} else {
Bundle arguments = getArguments();
if (arguments != null) {
file = getArguments().getParcelable(ARG_FILE);
account = getArguments().getParcelable(ARG_ACCOUNT);
}
}
if (file == null) {
throw new IllegalArgumentException("File may not be null");
}
if (account == null) {
throw new IllegalArgumentException("Account may not be null");
}
fileDisplayActivity = (FileDisplayActivity) getActivity();
if (fileDisplayActivity == null) {
throw new IllegalArgumentException("FileActivity may not be null");
}
fileOperationsHelper = fileDisplayActivity.getFileOperationsHelper();
fileDataStorageManager = fileDisplayActivity.getStorageManager();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
refreshCapabilitiesFromDB();
refreshPublicShareFromDB();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.file_details_sharing_fragment, container, false);
unbinder = ButterKnife.bind(this, view);
if (fileDataStorageManager == null) {
fileDataStorageManager = new FileDataStorageManager(account, fileDisplayActivity.getContentResolver());
}
setupView();
return view;
}
@Override
public void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (!(getActivity() instanceof FileActivity)) {
throw new IllegalArgumentException("Calling activity must be of type FileActivity");
}
}
private void setupView() {
setShareWithYou();
if (file.canReshare()) {
setShareByLinkInfo(file.isSharedViaLink());
setShareWithUserInfo();
FileDetailSharingFragmentHelper.setupSearchView(
(SearchManager) fileDisplayActivity.getSystemService(Context.SEARCH_SERVICE), searchView,
fileDisplayActivity.getComponentName());
ThemeUtils.themeSearchView(getContext(), searchView, false);
} else {
searchView.setVisibility(View.GONE);
shareByLinkContainer.setVisibility(View.GONE);
noList.setText(R.string.reshare_not_allowed);
}
}
/**
* Updates Share by link UI
*
* @param isShareByLink flag is share by link is enable
*/
public void setShareByLinkInfo(boolean isShareByLink) {
shareByLink.setChecked(isShareByLink);
if (isShareByLink) {
shareLinkCopyIcon.setVisibility(View.VISIBLE);
} else {
shareLinkCopyIcon.setVisibility(View.INVISIBLE);
}
int accentColor = ThemeUtils.primaryAccentColor(getContext());
ThemeUtils.tintCheckbox(shareByLink, accentColor);
ThemeUtils.tintCheckbox(shareByLinkAllowEditing, accentColor);
setLinkDetailVisible(isShareByLink);
}
private void setLinkDetailVisible(boolean visible) {
if (visible) {
shareByLinkAllowEditing.setVisibility(View.VISIBLE);
overflowMenuShareLink.setVisibility(View.VISIBLE);
} else {
shareByLinkAllowEditing.setVisibility(View.INVISIBLE);
overflowMenuShareLink.setVisibility(View.INVISIBLE);
}
}
/**
* Update Share With data
*/
public void setShareWithUserInfo() {
// Get Users and Groups
shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), account.name);
// Update list of users/groups
updateListOfUserGroups();
}
private void setShareWithYou() {
if (accountManager.accountOwnsFile(file, account)) {
sharedWithYouContainer.setVisibility(View.GONE);
} else {
sharedWithYouUsername.setText(
String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName()));
DisplayUtils.setAvatar(account, file.getOwnerId(), this, getResources().getDimension(
R.dimen.file_list_item_avatar_icon_radius), getResources(), sharedWithYouAvatar,
getContext());
sharedWithYouAvatar.setVisibility(View.VISIBLE);
String note = file.getNote();
if (!TextUtils.isEmpty(note)) {
sharedWithYouNote.setText(file.getNote());
sharedWithYouNote.setVisibility(View.VISIBLE);
} else {
sharedWithYouNote.setVisibility(View.GONE);
}
}
}
private void updateListOfUserGroups() {
// TODO Refactoring: create a new {@link ShareUserListAdapter} instance with every call should not be needed
if (shares.size() > 0) {
AccountManager accountManager = AccountManager.get(getContext());
String userId = accountManager.getUserData(account,
com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID);
usersList.setVisibility(View.VISIBLE);
usersList.setAdapter(new UserListAdapter(fileDisplayActivity.getSupportFragmentManager(),
fileDisplayActivity, shares, account, file, this, userId));
usersList.setLayoutManager(new LinearLayoutManager(getContext()));
usersList.addItemDecoration(new SimpleListItemDividerDecoration(getContext()));
noList.setVisibility(View.GONE);
} else {
usersList.setVisibility(View.GONE);
noList.setVisibility(View.VISIBLE);
}
}
@OnClick(R.id.share_by_link)
public void toggleShareByLink() {
if (shareByLink.isChecked()) {
createShareLink();
} else {
fileOperationsHelper.unshareFileViaLink(file);
}
}
private void createShareLink() {
if (capabilities != null && (capabilities.getFilesSharingPublicPasswordEnforced().isTrue() ||
capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue())) {
// password enforced by server, request to the user before trying to create
requestPasswordForShareViaLink(true,
capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue());
} else {
// create without password if not enforced by server or we don't know if enforced;
fileOperationsHelper.shareFileViaLink(file, null);
}
}
private void showSendLinkTo() {
if (file.isSharedViaLink()) {
if (TextUtils.isEmpty(file.getPublicLink())) {
fileOperationsHelper.getFileWithLink(file);
} else {
FileDisplayActivity.showShareLinkDialog(fileDisplayActivity, file.getPublicLink());
}
}
}
@OnClick({R.id.share_link_copy_icon})
public void copyLinkToClipboard() {
if (file.isSharedViaLink()) {
if (TextUtils.isEmpty(file.getPublicLink())) {
fileOperationsHelper.getFileWithLink(file);
} else {
ClipboardUtil.copyToClipboard(getActivity(), file.getPublicLink());
}
}
}
@OnClick(R.id.share_by_link_allow_editing)
public void toggleShareLinkAllowEditing() {
if (file.isSharedViaLink()) {
fileOperationsHelper.setUploadPermissionsToShare(file, shareByLinkAllowEditing.isChecked());
}
}
@OnClick(R.id.overflow_menu_share_link)
public void showLinkOverflowMenu() {
Context context = getContext();
if (context != null && ThemeUtils.themingEnabled(context)) {
// use grey as fallback for elements where custom theming is not available
context.getTheme().applyStyle(R.style.FallbackThemingTheme, true);
} else {
context = getActivity();
}
PopupMenu popup = new PopupMenu(context, overflowMenuShareLink);
popup.inflate(R.menu.file_detail_sharing_link_menu);
prepareOptionsMenu(popup.getMenu());
popup.setOnMenuItemClickListener(this::optionsItemSelected);
popup.show();
}
private void prepareOptionsMenu(Menu menu) {
Resources res = getResources();
SharingMenuHelper.setupHideFileListingMenuItem(
menu.findItem(R.id.action_hide_file_listing),
file.isFolder(),
shareByLinkAllowEditing.isChecked(),
publicShare.getPermissions()
);
SharingMenuHelper.setupHideFileDownload(menu.findItem(R.id.action_hide_file_download),
publicShare.isHideFileDownload(), capabilities);
SharingMenuHelper.setupPasswordMenuItem(
menu.findItem(R.id.action_password),
publicShare.isPasswordProtected()
);
SharingMenuHelper.setupExpirationDateMenuItem(
menu.findItem(R.id.action_share_expiration_date),
publicShare.getExpirationDate(),
res
);
menu.findItem(R.id.action_share_send_note).setVisible(capabilities.getVersion().isNoteOnShareSupported());
}
private boolean optionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_hide_file_listing: {
item.setChecked(!item.isChecked());
if (capabilities.getFilesFileDrop().isTrue()) {
setHideFileListingPermissionsToShare(publicShare, item.isChecked());
} else {
// not supported in ownCloud
showNotSupportedByOcMessage();
}
return true;
}
case R.id.action_hide_file_download:
item.setChecked(!item.isChecked());
setHideFileDownloadPermissionToShare(file, item.isChecked());
return true;
case R.id.action_password: {
requestPasswordForShareViaLink(false,
capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue());
return true;
}
case R.id.action_share_expiration_date: {
ExpirationDatePickerDialogFragment dialog = ExpirationDatePickerDialogFragment.newInstance(file, -1);
dialog.show(
fileDisplayActivity.getSupportFragmentManager(),
ExpirationDatePickerDialogFragment.DATE_PICKER_DIALOG
);
return true;
}
case R.id.action_share_send_link: {
if(shareByLink.isChecked() && file.isSharedViaLink() && !TextUtils.isEmpty(file.getPublicLink())) {
FileDisplayActivity.showShareLinkDialog(fileDisplayActivity, file.getPublicLink());
} else {
showSendLinkTo();
}
return true;
}
case R.id.action_share_send_note:
NoteDialogFragment dialog = NoteDialogFragment.newInstance(publicShare);
dialog.show(fileDisplayActivity.getSupportFragmentManager(), NoteDialogFragment.NOTE_FRAGMENT);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void setHideFileListingPermissionsToShare(OCShare share, boolean hideFileListing) {
fileOperationsHelper.setHideFileListingPermissionsToShare(share, hideFileListing);
}
@Override
public void setHideFileDownloadPermissionToShare(OCFile file, boolean hideFileDownload) {
fileOperationsHelper.setHideFileDownloadPermissionsToShare(file, hideFileDownload);
}
@Override
public void showNotSupportedByOcMessage() {
if (getView() != null) {
Snackbar.make(getView(), R.string.files_drop_not_supported, Snackbar.LENGTH_LONG)
.setAction(R.string.learn_more, v ->
DisplayUtils.startLinkIntent(requireActivity(), R.string.url_server_install))
.show();
}
}
/**
* Updates the UI after the result of an update operation on the edited {@link OCFile}.
*
* @param result {@link RemoteOperationResult} of an update on the edited {@link OCFile} sharing information.
* @param file the edited {@link OCFile}
*/
public void onUpdateShareInformation(RemoteOperationResult result, OCFile file) {
this.file = file;
if (result.isSuccess()) {
refreshUiFromDB();
} else {
setupView();
}
}
/**
* Get {@link OCShare} instance from DB and updates the UI.
*/
private void refreshUiFromDB() {
if (publicShare != null) {
// Get edited shared by link
publicShare = fileDataStorageManager.getShareById(publicShare.getId());
}
// Updates UI with new state
setupView();
}
@Override
public void unshareWith(OCShare share) {
fileOperationsHelper.unshareFileWithUserOrGroup(file, share.getShareType(), share.getShareWith());
}
@Override
public int updatePermissionsToShare(OCShare share, boolean canReshare, boolean canEdit, boolean canEditCreate,
boolean canEditChange, boolean canEditDelete) {
SharePermissionsBuilder spb = new SharePermissionsBuilder();
spb.setSharePermission(canReshare);
if (file.isFolder()) {
spb.setUpdatePermission(canEditChange)
.setCreatePermission(canEditCreate)
.setDeletePermission(canEditDelete);
} else {
spb.setUpdatePermission(canEdit);
}
int permissions = spb.build();
fileOperationsHelper.setPermissionsToShare(share, permissions);
return permissions;
}
@Override
public void updateNoteToShare(OCShare share, String note) {
fileOperationsHelper.updateNoteToShare(share, note);
}
/**
* Starts a dialog that requests a password to the user to protect a share link.
*
* @param createShare When 'true', the request for password will be followed by the creation of a new public
* link; when 'false', a public share is assumed to exist, and the password is bound to it.
* @param askForPassword if true, password is optional
*/
public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) {
SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(file,
createShare,
askForPassword);
dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
}
@Override
public void requestPasswordForShare(OCShare share) {
SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share);
dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT);
}
/**
* Get known server capabilities from DB
*/
public void refreshCapabilitiesFromDB() {
capabilities = fileDataStorageManager.getCapability(account.name);
}
/**
* Get public link from the DB to fill in the "Share link" section in the UI.
*
* Takes into account server capabilities before reading database.
*/
public void refreshPublicShareFromDB() {
if (FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities) || !file.canReshare()) {
shareByLinkContainer.setVisibility(View.GONE);
} else {
// Get public share
publicShare = fileDataStorageManager.getFirstShareByPathAndType(file.getRemotePath(),
ShareType.PUBLIC_LINK, "");
// Update public share section
updatePublicShareSection();
}
}
/**
* Updates in the UI the section about public share with the information
* in the current public share bound to, if any.
*/
private void updatePublicShareSection() {
if (publicShare != null && ShareType.PUBLIC_LINK.equals(publicShare.getShareType())) {
shareByLink.setChecked(true);
if (publicShare.getPermissions() > OCShare.READ_PERMISSION_FLAG) {
shareByLinkAllowEditing.setChecked(true);
} else {
shareByLinkAllowEditing.setChecked(false);
}
setShareByLinkInfo(true);
} else {
setShareByLinkInfo(false);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(FileActivity.EXTRA_FILE, file);
outState.putParcelable(FileActivity.EXTRA_ACCOUNT, account);
}
@Override
public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
sharedWithYouAvatar.setImageDrawable(avatarDrawable);
}
@Override
public boolean shouldCallGeneratedCallback(String tag, Object callContext) {
return false;
}
}