/* * Nextcloud Android client application * * @author Andy Scherzinger * @author Chris Narkiewicz * * Copyright (C) 2018 Andy Scherzinger * Copyright (C) 2020 Chris Narkiewicz * * 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.AccountManager; import android.app.SearchManager; import android.content.Context; import android.content.res.Resources; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.InputType; 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.TextView; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.OwnCloudAccount; 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.PublicShareInterface; import com.owncloud.android.ui.adapter.PublicShareListAdapter; import com.owncloud.android.ui.adapter.ShareeListAdapter; 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.RenamePublicShareDialogFragment; 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.PopupMenu; 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 ShareeListAdapter.ShareeListAdapterListener, DisplayUtils.AvatarGenerationListener, PublicShareInterface, Injectable { private static final String ARG_FILE = "FILE"; private static final String ARG_USER = "USER"; public static final int PERMISSION_EDITING_ALLOWED = 17; private OCFile file; private User user; private OCCapability capabilities; private FileOperationsHelper fileOperationsHelper; private FileActivity fileActivity; private FileDataStorageManager fileDataStorageManager; private Unbinder unbinder; @BindView(R.id.searchView) SearchView searchView; @BindView(R.id.shareUsersList) RecyclerView usersList; @BindView(R.id.publicShareList) RecyclerView publicShareList; @BindView(R.id.new_public_share) View addPublicShare; @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_container) View sharedWithYouNoteContainer; @BindView(R.id.shared_with_you_note) TextView sharedWithYouNote; @BindView(R.id.copy_internal_link_icon) ImageView internalLinkIcon; @BindView(R.id.shareInternalLinkText) TextView internalLinkText; @Inject UserAccountManager accountManager; public static FileDetailSharingFragment newInstance(OCFile file, User user) { FileDetailSharingFragment fragment = new FileDetailSharingFragment(); Bundle args = new Bundle(); args.putParcelable(ARG_FILE, file); args.putParcelable(ARG_USER, user); fragment.setArguments(args); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { file = savedInstanceState.getParcelable(ARG_FILE); user = savedInstanceState.getParcelable(ARG_USER); } else { Bundle arguments = getArguments(); if (arguments != null) { file = getArguments().getParcelable(ARG_FILE); user = getArguments().getParcelable(ARG_USER); } } if (file == null) { throw new IllegalArgumentException("File may not be null"); } if (user == null) { throw new IllegalArgumentException("Account may not be null"); } fileActivity = (FileActivity) getActivity(); if (fileActivity == null) { throw new IllegalArgumentException("FileActivity may not be null"); } } @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); fileOperationsHelper = fileActivity.getFileOperationsHelper(); fileDataStorageManager = fileActivity.getStorageManager(); setupView(); // todo extract internalLinkIcon.getBackground().setColorFilter(getResources().getColor(R.color.grey_db), PorterDuff.Mode.SRC_IN); internalLinkIcon.getDrawable().mutate().setColorFilter(getResources().getColor(R.color.black), PorterDuff.Mode.SRC_IN); if (file.isFolder()) { internalLinkText.setText(getString(R.string.share_internal_link_to_folder_text)); } else { internalLinkText.setText(getString(R.string.share_internal_link_to_file_text)); } return view; } @Override public void onDestroy() { super.onDestroy(); unbinder.unbind(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (!(getActivity() instanceof FileActivity)) { throw new IllegalArgumentException("Calling activity must be of type FileActivity"); } } private void setupView() { setShareWithYou(); FileDetailSharingFragmentHelper.setupSearchView( (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE), searchView, fileActivity.getComponentName()); ThemeUtils.themeSearchView(searchView, requireContext()); if (file.canReshare()) { setShareWithUserInfo(); } else { searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed)); searchView.setInputType(InputType.TYPE_NULL); disableSearchView(searchView); } } private void disableSearchView(View view) { view.setEnabled(false); if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { disableSearchView(viewGroup.getChildAt(i)); } } } private void setShareWithYou() { if (accountManager.userOwnsFile(file, user)) { sharedWithYouContainer.setVisibility(View.GONE); } else { sharedWithYouUsername.setText( String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName())); DisplayUtils.setAvatar(user, 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()); sharedWithYouNoteContainer.setVisibility(View.VISIBLE); } else { sharedWithYouNoteContainer.setVisibility(View.GONE); } } } private void setShareWithUserInfo() { // TODO Refactoring: create a new {@link ShareUserListAdapter} instance with every call should not be needed // to show share with users/groups info List shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), user.toPlatformAccount().name); if (shares.size() > 0) { AccountManager accountManager = AccountManager.get(getContext()); String userId = accountManager.getUserData(user.toPlatformAccount(), com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); usersList.setVisibility(View.VISIBLE); usersList.setAdapter(new ShareeListAdapter(fileActivity.getSupportFragmentManager(), fileActivity, shares, user.toPlatformAccount(), file, this, userId)); usersList.setLayoutManager(new LinearLayoutManager(getContext())); usersList.addItemDecoration(new SimpleListItemDividerDecoration(getContext())); } else { usersList.setVisibility(View.GONE); } } @OnClick(R.id.copy_internal_container) public void copyInternalLink() { OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); if (account == null) { DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_url)); return; } FileDisplayActivity.showShareLinkDialog(fileActivity, file, createInternalLink(account, file)); } private String createInternalLink(OwnCloudAccount account, OCFile file) { return account.getBaseUri() + "/index.php/f/" + file.getLocalId(); } 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.shareFileViaPublicShare(file, null); } } private void showSendLinkTo() { if (file.isSharedViaLink()) { if (TextUtils.isEmpty(file.getPublicLink())) { fileOperationsHelper.getFileWithLink(file); } else { FileDisplayActivity.showShareLinkDialog(fileActivity, file, file.getPublicLink()); } } } public void copyLink(OCShare share) { if (file.isSharedViaLink()) { if (TextUtils.isEmpty(file.getPublicLink())) { fileOperationsHelper.getFileWithLink(file); } else { ClipboardUtil.copyToClipboard(getActivity(), file.getPublicLink()); } } } public void showLinkOverflowMenu(OCShare publicShare, ImageView overflowMenuShareLink) { if (ThemeUtils.themingEnabled(requireContext())) { // use grey as fallback for elements where custom theming is not available requireContext().getTheme().applyStyle(R.style.FallbackThemingTheme, true); } PopupMenu popup = new PopupMenu(requireContext(), overflowMenuShareLink); popup.inflate(R.menu.fragment_file_detail_sharing_link); prepareOptionsMenu(popup.getMenu(), publicShare); popup.setOnMenuItemClickListener(menuItem -> optionsItemSelected(menuItem, publicShare)); popup.show(); } private void prepareOptionsMenu(Menu menu, OCShare publicShare) { Resources res = requireContext().getResources(); SharingMenuHelper.setupHideFileListingMenuItem(menu.findItem(R.id.action_hide_file_listing), file.isFolder(), menu.findItem(R.id.action_allow_editing).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()); if (publicShare.getPermissions() > PERMISSION_EDITING_ALLOWED) { menu.findItem(R.id.action_allow_editing).setChecked(true); } else { menu.findItem(R.id.action_allow_editing).setChecked(false); } } public boolean optionsItemSelected(MenuItem item, OCShare publicShare) { switch (item.getItemId()) { case R.id.action_allow_editing: if (file.isSharedViaLink()) { item.setChecked(!item.isChecked()); fileOperationsHelper.setUploadPermissionsToPublicShare(publicShare, item.isChecked()); } return true; case R.id.action_hide_file_listing: { item.setChecked(!item.isChecked()); fileOperationsHelper.setHideFileListingPermissionsToPublicShare(publicShare, item.isChecked()); return true; } case R.id.action_hide_file_download: item.setChecked(!item.isChecked()); fileOperationsHelper.setHideFileDownloadPermissionsToPublicShare(publicShare, item.isChecked()); return true; case R.id.action_edit_label: RenamePublicShareDialogFragment renameDialog = RenamePublicShareDialogFragment.newInstance(publicShare); renameDialog.show(fileActivity.getSupportFragmentManager(), RenamePublicShareDialogFragment.RENAME_PUBLIC_SHARE_FRAGMENT); return true; case R.id.action_password: { requestPasswordForShare(publicShare, capabilities.getFilesSharingPublicAskForOptionalPassword().isTrue()); return true; } case R.id.action_share_expiration_date: { ExpirationDatePickerDialogFragment expirationDialog = ExpirationDatePickerDialogFragment .newInstance(publicShare, publicShare.getExpirationDate()); expirationDialog.show(fileActivity.getSupportFragmentManager(), ExpirationDatePickerDialogFragment.DATE_PICKER_DIALOG); return true; } case R.id.action_share_send_link: { if (file.isSharedViaLink() && !TextUtils.isEmpty(file.getPublicLink())) { FileDisplayActivity.showShareLinkDialog(fileActivity, file, file.getPublicLink()); } else { showSendLinkTo(); } return true; } case R.id.action_share_send_note: NoteDialogFragment noteDialog = NoteDialogFragment.newInstance(publicShare); noteDialog.show(fileActivity.getSupportFragmentManager(), NoteDialogFragment.NOTE_FRAGMENT); return true; case R.id.action_add_another_public_share_link: createShareLink(); return true; case R.id.action_unshare: fileOperationsHelper.unshareShare(file, publicShare); return true; default: return super.onOptionsItemSelected(item); } } /** * 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() { // Updates UI with new state setupView(); } @Override public void unshareWith(OCShare share) { fileOperationsHelper.unshareShare(file, share); } @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; } /** * 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, boolean askForPassword) { SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword); dialog.show(getChildFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); } /** * Get known server capabilities from DB */ public void refreshCapabilitiesFromDB() { capabilities = fileDataStorageManager.getCapability(user.getAccountName()); } /** * 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()) { publicShareList.setVisibility(View.GONE); return; } // Get public share List shares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), ShareType.PUBLIC_LINK, ""); if (shares.isEmpty()) { addPublicShare.setVisibility(View.VISIBLE); publicShareList.setVisibility(View.GONE); ImageView icon = requireView().findViewById(R.id.copy_internal_link_icon); icon.getBackground().setColorFilter(requireContext() .getResources() .getColor(R.color.primary_button_background_color), PorterDuff.Mode.SRC_IN); icon.getDrawable().mutate().setColorFilter(requireContext().getResources().getColor(R.color.black), PorterDuff.Mode.SRC_IN); requireView().findViewById(R.id.add_new_public_share_link).setOnClickListener(v -> createShareLink()); } else { addPublicShare.setVisibility(View.GONE); publicShareList.setVisibility(View.VISIBLE); publicShareList.setAdapter(new PublicShareListAdapter(getContext(), shares, this)); publicShareList.setLayoutManager(new LinearLayoutManager(getContext())); publicShareList.addItemDecoration(new SimpleListItemDividerDecoration(getContext())); } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable(ARG_FILE, file); outState.putParcelable(ARG_USER, user); } @Override public void avatarGenerated(Drawable avatarDrawable, Object callContext) { sharedWithYouAvatar.setImageDrawable(avatarDrawable); } @Override public boolean shouldCallGeneratedCallback(String tag, Object callContext) { return false; } }