diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 111a0dc01..2f8eb23ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,7 +58,7 @@ + android:name=".itemdetails.ItemDetailsActivity" /> diff --git a/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt new file mode 100644 index 000000000..73b39da8c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/activities/ActivitiesFragment.kt @@ -0,0 +1,31 @@ +package it.niedermann.owncloud.notes.activities + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.FragmentActivitiesBinding + +class ActivitiesFragment : BrandedFragment() { + + lateinit var binding : FragmentActivitiesBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + binding = FragmentActivitiesBinding.inflate(inflater) + return super.onCreateView(inflater, container, savedInstanceState) + } + + override fun applyBrand(color: Int) { + val util = BrandingUtil.of(color, requireContext()) + util.platform.colorViewBackground(binding.getRoot()) + } + + companion object { + fun newInstance() : ActivitiesFragment { + return ActivitiesFragment() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index f64eec2e7..201ded2ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -46,6 +46,7 @@ import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener; import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment; import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; @@ -305,9 +306,10 @@ public boolean onOptionsItemSelected(MenuItem item) { .newInstance(new ArrayList<>(repo.getAccounts()), note.getAccountId()) .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; - } else if (itemId == R.id.menu_share) { - shareNote(); - return false; + } else if (itemId == R.id.menu_details) { + Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(requireContext(), note, localAccount); + startActivity(detailsIntent); + return true; } else if (itemId == MENU_ID_PIN) { pinNoteToHome(); return true; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java index 7a1126f59..9e17cc55e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java @@ -28,7 +28,7 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) { menu.findItem(R.id.menu_preview).setVisible(false); menu.findItem(R.id.menu_cancel).setVisible(false); menu.findItem(R.id.menu_delete).setVisible(false); - menu.findItem(R.id.menu_share).setVisible(false); + menu.findItem(R.id.menu_details).setVisible(false); menu.findItem(R.id.menu_move).setVisible(false); menu.findItem(R.id.menu_category).setVisible(false); menu.findItem(R.id.menu_title).setVisible(false); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt new file mode 100644 index 000000000..fbb882068 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/itemdetails/ItemDetailsActivity.kt @@ -0,0 +1,120 @@ +package it.niedermann.owncloud.notes.itemdetails + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.text.format.DateUtils +import androidx.appcompat.content.res.AppCompatResources +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.google.android.material.tabs.TabLayoutMediator +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.activities.ActivitiesFragment +import it.niedermann.owncloud.notes.branding.BrandedActivity +import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActivityItemDetailsBinding +import it.niedermann.owncloud.notes.persistence.entity.Account +import it.niedermann.owncloud.notes.persistence.entity.Note +import it.niedermann.owncloud.notes.share.NoteShareFragment +import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument + +class ItemDetailsActivity : BrandedActivity() { + + lateinit var binding : ActivityItemDetailsBinding + lateinit var note: Note + lateinit var account: Account + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + parseArguments() + + binding = ActivityItemDetailsBinding.inflate(layoutInflater) + setContentView(binding.root) + + loadHeaderData() + loadFragments() + } + + private fun parseArguments() { + note = intent.extras.getSerializableArgument(ARG_NOTE, Note::class.java) + ?: throw RuntimeException("Argument $ARG_NOTE is mandatory") + account = intent.extras.getSerializableArgument(ARG_ACCOUNT, Account::class.java) + ?: throw RuntimeException("Argument $ARG_ACCOUNT is mandatory") + } + + private fun loadHeaderData() { + binding.filename.text = note.title + binding.favorite.setImageResource( + if (note.favorite) R.drawable.ic_star_yellow_24dp + else R.drawable.ic_star_border_grey_ccc_24dp + ) + binding.size.text = "TODO" + binding.lastModificationTimestamp.text = note.modified?.let { + DateUtils.formatDateTime( + this, + it.timeInMillis, + DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME + ) + } + } + + private fun loadFragments() { + val pages = listOf( + Page( + getString(R.string.activities_title), + AppCompatResources.getDrawable(this, R.drawable.nav_activity), + ActivitiesFragment.newInstance() + ), + Page( + getString(R.string.share), + AppCompatResources.getDrawable(this, R.drawable.ic_share_white_24dp), + NoteShareFragment.newInstance(note, account) + ) + ) + + binding.pager.adapter = PagerAdapter(pages) + TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> + tab.text = pages[position].title + tab.icon = pages[position].icon + }.attach() + } + + override fun applyBrand(color: Int) { + val util = BrandingUtil.of(color, this) + util.platform.colorViewBackground(binding.getRoot()) + util.platform.colorViewBackground(binding.tabLayout) + util.material.themeTabLayout(binding.tabLayout) + } + + private data class Page ( + val title: String, + val icon: Drawable?, + val fragment: BrandedFragment + ) + + private inner class PagerAdapter(val fragments: List) : + FragmentStateAdapter(this@ItemDetailsActivity) + { + override fun getItemCount() = fragments.size + override fun createFragment(position: Int) = fragments[position].fragment + } + + companion object { + const val ARG_NOTE: String = "note" + const val ARG_ACCOUNT: String = "account" + + @JvmStatic + fun getActivityStartIntent(context: Context, note: Note, account: Account): Intent { + val bundle = Bundle() + bundle.putSerializable(ARG_NOTE, note) + bundle.putSerializable(ARG_ACCOUNT, account) + + val intent = Intent(context, ItemDetailsActivity::class.java) + intent.putExtras(bundle) + return intent + } + } + +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index ed7b84ca6..f5c07fe61 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -100,6 +100,7 @@ import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.main.items.ItemAdapter; import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration; import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper; @@ -114,7 +115,6 @@ import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; -import it.niedermann.owncloud.notes.share.NoteShareActivity; import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; @@ -915,10 +915,10 @@ public void onNoteFavoriteClick(int position, View view) { } @Override - public void openShare(int position) { + public void openDetails(int position) { if (adapter.getItem(position) instanceof Note note) { mainViewModel.getCurrentAccount().observe(this, (account) -> { - final Intent intent = NoteShareActivity.getActivityStartIntent(note, account, this); + final Intent intent = ItemDetailsActivity.getActivityStartIntent(this, note, account); startActivity(intent); }); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index a80ac4325..0e49e6970 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -8,7 +8,6 @@ import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; @@ -33,7 +32,7 @@ import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedSnackbar; import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment; -import it.niedermann.owncloud.notes.share.NoteShareActivity; +import it.niedermann.owncloud.notes.itemdetails.ItemDetailsActivity; import it.niedermann.owncloud.notes.shared.util.ShareUtil; public class MultiSelectedActionModeCallback implements Callback { @@ -102,6 +101,8 @@ public boolean onCreateActionMode(ActionMode mode, Menu menu) { @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // Info menu makes sense only for single selection + menu.findItem(R.id.menu_details).setEnabled(tracker.getSelection().size() == 1); return false; } @@ -159,23 +160,12 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { selection.add(sel); } tracker.clearSelection(); - - if (selection.size() == 1) { - final var currentAccount$ = mainViewModel.getCurrentAccount(); - currentAccount$.observe(lifecycleOwner, account -> { - currentAccount$.removeObservers(lifecycleOwner); - executor.submit(() -> {{ - final var note = mainViewModel.getFullNote(selection.get(0)); - final Intent intent = NoteShareActivity.getActivityStartIntent(note, account, mainActivity); - mainActivity.startActivity(intent); - }}); - }); - } else { - ShareUtil.openShareDialog(context, + executor.submit(() -> { + final var noteContents = mainViewModel.collectNoteContents(selection); + mainActivity.runOnUiThread(() -> ShareUtil.openShareDialog(context, context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), - mainViewModel.collectNoteContents(selection)); - } - + noteContents)); + }); return true; } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it final var accountLiveData = mainViewModel.getCurrentAccount(); @@ -186,6 +176,24 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { .show(fragmentManager, CategoryDialogFragment.class.getSimpleName()); }); return true; + } else if (itemId == R.id.menu_details) { + final var selection = new ArrayList(tracker.getSelection().size()); + for (final var sel : tracker.getSelection()) { + selection.add(sel); + } + tracker.clearSelection(); + + if (selection.size() == 1) { + final var currentAccount$ = mainViewModel.getCurrentAccount(); + currentAccount$.observe(lifecycleOwner, account -> { + currentAccount$.removeObservers(lifecycleOwner); + executor.submit(() -> {{ + final var note = mainViewModel.getFullNote(selection.get(0)); + Intent detailsIntent = ItemDetailsActivity.getActivityStartIntent(context, note, account); + mainActivity.startActivity(detailsIntent); + }}); + }); + } } return false; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java index 706c5bd26..d68ac162a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java @@ -114,16 +114,11 @@ protected void bindSearchableContent(@NonNull Context context, @NonNull TextView } } - protected void bindNoteSharedIcon(@NonNull Context context, boolean shared, @NonNull MaterialButton noteShareIcon, int color) { - if (shared) { - noteShareIcon.setVisibility(VISIBLE); - noteShareIcon.setOnClickListener(view -> noteClickListener.openShare(getLayoutPosition())); - } else { - noteShareIcon.setVisibility(GONE); - } + protected void bindNoteDetailsIcon(@NonNull Context context, @NonNull MaterialButton noteDetailsIcon, int color) { + noteDetailsIcon.setOnClickListener(view -> noteClickListener.openDetails(getLayoutPosition())); final var util = BrandingUtil.of(color, context); - util.material.colorMaterialButtonContent(noteShareIcon, ColorRole.PRIMARY); + util.material.colorMaterialButtonContent(noteDetailsIcon, ColorRole.SECONDARY); } public abstract void showSwipe(float dX); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java index 67c5c53d9..0f5505e74 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java @@ -55,7 +55,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color); - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java index 0699dd237..597b86d22 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java @@ -46,7 +46,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, i bindFavorite(binding.noteFavorite, note.getFavorite()); bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); } @Nullable diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java index 771db1737..f67ccb469 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java @@ -74,7 +74,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), color); } - bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); + bindNoteDetailsIcon(context, binding.noteDetails, color); } @NonNull diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java deleted file mode 100644 index a69cc8c0c..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java +++ /dev/null @@ -1,903 +0,0 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.niedermann.owncloud.notes.share; - -import android.Manifest; -import android.app.Activity; -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.text.InputType; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; - -import androidx.activity.OnBackPressedCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.widget.SearchView; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.LinearLayoutManager; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.android.sso.helper.SingleAccountHelper; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.lib.resources.shares.ShareType; - -import org.jetbrains.annotations.NotNull; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedActivity; -import it.niedermann.owncloud.notes.branding.BrandedSnackbar; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding; -import it.niedermann.owncloud.notes.main.MainActivity; -import it.niedermann.owncloud.notes.persistence.ApiResult; -import it.niedermann.owncloud.notes.persistence.ApiResultKt; -import it.niedermann.owncloud.notes.persistence.entity.Account; -import it.niedermann.owncloud.notes.persistence.entity.Note; -import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter; -import it.niedermann.owncloud.notes.share.adapter.SuggestionAdapter; -import it.niedermann.owncloud.notes.share.dialog.NoteShareActivityShareItemActionBottomSheetDialog; -import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog; -import it.niedermann.owncloud.notes.share.dialog.ShareLinkToDialog; -import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment; -import it.niedermann.owncloud.notes.share.helper.AvatarLoader; -import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider; -import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction; -import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; -import it.niedermann.owncloud.notes.share.model.CreateShareResponse; -import it.niedermann.owncloud.notes.share.model.CreateShareResponseExtensionsKt; -import it.niedermann.owncloud.notes.share.model.UpdateShareRequest; -import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; -import it.niedermann.owncloud.notes.share.repository.ShareRepository; -import it.niedermann.owncloud.notes.shared.model.Capabilities; -import it.niedermann.owncloud.notes.shared.model.OcsResponse; -import it.niedermann.owncloud.notes.shared.util.DisplayUtils; -import it.niedermann.owncloud.notes.shared.util.ShareUtil; -import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil; -import it.niedermann.owncloud.notes.shared.util.extensions.BundleExtensionsKt; -import it.niedermann.owncloud.notes.util.DateUtil; - -public class NoteShareActivity extends BrandedActivity implements ShareeListAdapterListener, NoteShareItemAction, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions, SharePasswordDialogFragment.SharePasswordDialogListener { - - private static final String TAG = "NoteShareActivity"; - public static final String ARG_NOTE = "NOTE"; - public static final String ARG_ACCOUNT = "ACCOUNT"; - public static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG"; - - private ScheduledExecutorService executorService; - private Future future; - private static final long SEARCH_DELAY_MS = 500; - - private ActivityNoteShareBinding binding; - private Note note; - private Account account; - private ShareRepository repository; - private Capabilities capabilities; - private ActivityResultLauncher resultLauncher; - private final List shares = Collections.synchronizedList(new ArrayList<>()); - - public static Intent getActivityStartIntent(@NonNull Note note, @NonNull Account account, @NonNull Activity activity) { - Bundle bundle = new Bundle(); - bundle.putSerializable(NoteShareActivity.ARG_NOTE, note); - bundle.putSerializable(NoteShareActivity.ARG_ACCOUNT, account); - - Intent intent = new Intent(activity, NoteShareActivity.class); - intent.putExtras(bundle); - return intent; - } - - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - executorService = Executors.newSingleThreadScheduledExecutor(); - binding = ActivityNoteShareBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - binding.toolbar.setNavigationOnClickListener(v -> backPressed()); - registerResultLauncher(); - initializeArguments(); - initializeOnBackPressedDispatcher(); - } - - private void registerResultLauncher() { - resultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && result.getData() != null) { - recreate(); - } - }); - } - - private void initializeOnBackPressedDispatcher() { - getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - backPressed(); - } - }); - } - - private void backPressed() { - Intent intent = new Intent(NoteShareActivity.this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivity(intent); - finish(); - } - - private void initializeArguments() { - Bundle bundler = getIntent().getExtras(); - note = BundleExtensionsKt.getSerializableArgument(bundler, ARG_NOTE, Note.class); - account = BundleExtensionsKt.getSerializableArgument(bundler, ARG_ACCOUNT, Account.class); - - if (note == null) { - throw new IllegalArgumentException("Note cannot be null"); - } - - if (account == null) { - throw new IllegalArgumentException("Account cannot be null"); - } - - executorService.submit(() -> { - try { - final var ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(NoteShareActivity.this); - repository = new ShareRepository(NoteShareActivity.this, ssoAcc); - capabilities = repository.getCapabilities(); - repository.fetchSharesForNotesAndSaveShareEntities(); - - runOnUiThread(() -> { - binding.fileName.setText(note.getTitle()); - binding.searchContainer.setVisibility(View.VISIBLE); - binding.sharesList.setVisibility(View.VISIBLE); - binding.sharesList.setAdapter(new ShareeListAdapter(this, new ArrayList<>(), this, account)); - binding.sharesList.setLayoutManager(new LinearLayoutManager(this)); - binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); - binding.btnShareButton.setOnClickListener(v -> ShareUtil.openShareDialog(this, note.getTitle(), note.getContent())); - - if (note.getReadonly()) { - setupReadOnlySearchView(); - } else { - setupSearchView((SearchManager) getSystemService(Context.SEARCH_SERVICE), getComponentName()); - } - - updateShareeListAdapter(); - binding.loadingLayout.setVisibility(View.GONE); - }); - } catch (Exception e) { - Log_OC.e(TAG, "Exception at NoteShareActivity.init: " + e); - } - }); - } - - @Override - public void onStart() { - super.onStart(); - - // note cannot be encrypted - logic ported from files app - UsersAndGroupsSearchConfig.INSTANCE.setSearchOnlyUsers(false); - } - - @Override - public void onStop() { - super.onStop(); - UsersAndGroupsSearchConfig.INSTANCE.reset(); - } - - private void disableSearchView(View view) { - view.setEnabled(false); - - if (view instanceof ViewGroup viewGroup) { - for (int i = 0; i < viewGroup.getChildCount(); i++) { - disableSearchView(viewGroup.getChildAt(i)); - } - } - } - - private void setupReadOnlySearchView() { - binding.searchView.setIconifiedByDefault(false); - binding.searchView.setQueryHint(getResources().getString(R.string.note_share_activity_resharing_not_allowed)); - binding.searchView.setInputType(InputType.TYPE_NULL); - binding.pickContactEmailBtn.setVisibility(View.GONE); - disableSearchView(binding.searchView); - } - - private void setupSearchView(@Nullable SearchManager searchManager, ComponentName componentName) { - if (searchManager == null) { - binding.searchView.setVisibility(View.GONE); - return; - } - - SuggestionAdapter suggestionAdapter = new SuggestionAdapter(this, null, account); - UsersAndGroupsSearchProvider provider = new UsersAndGroupsSearchProvider(this, repository); - - binding.searchView.setSuggestionsAdapter(suggestionAdapter); - binding.searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)); - binding.searchView.setIconifiedByDefault(false); - binding.searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); - binding.searchView.setQueryHint(getResources().getString(R.string.note_share_activity_search_text)); - binding.searchView.setInputType(InputType.TYPE_CLASS_TEXT); - binding.searchView.setFocusable(true); - binding.searchView.setFocusableInTouchMode(true); - - View closeButton = binding.searchView.findViewById(androidx.appcompat.R.id.search_close_btn); - closeButton.setOnClickListener(v -> { - binding.progressBar.setVisibility(View.GONE); - binding.searchView.setQuery("", false); - }); - - binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - // return true to prevent the query from being processed; - return true; - } - - @Override - public boolean onQueryTextChange(String newText) { - if (!newText.isEmpty()) { - binding.progressBar.setVisibility(View.VISIBLE); - - // Cancel the previous task if it's still running - if (future != null && !future.isDone()) { - future.cancel(true); - } - - // Schedule a new task with a delay - future = executorService.schedule(() -> { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return; - } - - final var isFederationShareAllowed = capabilities.getFederationShare(); - try { - var cursor = provider.searchForUsersOrGroups(newText, isFederationShareAllowed); - - if (cursor == null || cursor.getCount() == 0) { - return; - } - - runOnUiThread(() -> { - if (binding.searchView.getVisibility() == View.VISIBLE) { - suggestionAdapter.swapCursor(cursor); - } - - binding.progressBar.setVisibility(View.GONE); - }); - - } catch (Exception e) { - Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: " + e); - runOnUiThread(() -> binding.progressBar.setVisibility(View.GONE)); - } - - }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS); - } else { - binding.progressBar.setVisibility(View.GONE); - suggestionAdapter.swapCursor(null); - } - return false; - } - }); - - binding.searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(binding.searchView.findFocus(), InputMethodManager.SHOW_IMPLICIT); - } - }); - - binding.searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { - @Override - public boolean onSuggestionSelect(int position) { - return false; - } - - @Override - public boolean onSuggestionClick(int position) { - Cursor cursor = suggestionAdapter.getCursor(); - if (cursor != null && cursor.moveToPosition(position)) { - String suggestion = cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)); - binding.searchView.setQuery(suggestion, false); - - String shareWith = cursor.getString(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_WITH)); - int shareType = cursor.getInt(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_TYPE)); - navigateNoteShareDetail(shareWith, shareType); - } - return true; - } - }); - } - - private void navigateNoteShareDetail(String shareWith, int shareType) { - Bundle bundle = new Bundle(); - - bundle.putSerializable(NoteShareDetailActivity.ARG_NOTE, note); - bundle.putString(NoteShareDetailActivity.ARG_SHAREE_NAME, shareWith); - bundle.putInt(NoteShareDetailActivity.ARG_SHARE_TYPE, shareType); - - Intent intent = new Intent(this, NoteShareDetailActivity.class); - intent.putExtras(bundle); - resultLauncher.launch(intent); - } - - private boolean accountOwnsFile() { - if (shares.isEmpty()) { - return true; - } - - final var share = shares.get(0); - String ownerDisplayName = share.getOwnerDisplayName(); - return TextUtils.isEmpty(ownerDisplayName) || account.getAccountName().split("@")[0].equalsIgnoreCase(ownerDisplayName); - } - - private void setShareWithYou() { - if (accountOwnsFile()) { - binding.sharedWithYouContainer.setVisibility(View.GONE); - } else { - if (shares.isEmpty()) { - return; - } - - final var share = shares.get(0); - - binding.sharedWithYouUsername.setText( - String.format(getString(R.string.note_share_activity_shared_with_you), share.getOwnerDisplayName())); - AvatarLoader.INSTANCE.load(this, binding.sharedWithYouAvatar, account); - binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); - - String description = share.getNote(); - - if (!TextUtils.isEmpty(description)) { - binding.sharedWithYouNote.setText(description); - binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); - } else { - binding.sharedWithYouNoteContainer.setVisibility(View.GONE); - } - } - } - - public void copyInternalLink() { - if (account == null) { - DisplayUtils.showSnackMessage(this, getString(R.string.note_share_activity_could_not_retrieve_url)); - return; - } - - final var link = createInternalLink(); - showShareLinkDialog(link); - } - - @Override - public void createPublicShareLink() { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return; - } - - if (capabilities.getPublicPasswordEnforced() || capabilities.getAskForOptionalPassword()) { - // password enforced by server, request to the user before trying to create - requestPasswordForShareViaLink(true, capabilities.getAskForOptionalPassword()); - } else { - executorService.submit(() -> { - final var result = repository.addShare(note, ShareType.PUBLIC_LINK, ""); - runOnUiThread(() -> { - if (result instanceof ApiResult.Success> successResponse && binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, successResponse.getMessage()); - note.setIsShared(true); - repository.updateNote(note); - adapter.addShare(CreateShareResponseExtensionsKt.toOCShare(successResponse.getData().ocs.data)); - } else if (result instanceof ApiResult.Error errorResponse) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, errorResponse.getMessage()); - } - }); - }); - } - } - - public void requestPasswordForShareViaLink(boolean createShare, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(note, createShare, askForPassword, this); - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - // TODO: - @Override - public void createSecureFileDrop() { - - } - - private void showShareLinkDialog(String link) { - Intent intentToShareLink = new Intent(Intent.ACTION_SEND); - - intentToShareLink.putExtra(Intent.EXTRA_TEXT, link); - intentToShareLink.setType("text/plain"); - intentToShareLink.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.note_share_activity_subject_shared_with_you, note.getTitle())); - - String[] packagesToExclude = new String[]{this.getPackageName()}; - DialogFragment chooserDialog = ShareLinkToDialog.newInstance(intentToShareLink, packagesToExclude); - chooserDialog.show(getSupportFragmentManager(), FTAG_CHOOSER_DIALOG); - } - - private String createInternalLink() { - Uri baseUri = Uri.parse(account.getUrl()); - return baseUri + "/index.php/f/" + note.getRemoteId(); - } - - @Override - public void copyLink(OCShare share) { - if (!note.isShared()) { - return; - } - - if (TextUtils.isEmpty(share.getShareLink())) { - copyAndShareFileLink(share.getShareLink()); - } else { - ClipboardUtil.copyToClipboard(this, share.getShareLink()); - } - } - - private void copyAndShareFileLink(String link) { - ClipboardUtil.copyToClipboard(this, link, false); - Snackbar snackbar = Snackbar - .make(this.findViewById(android.R.id.content), R.string.clipboard_text_copied, Snackbar.LENGTH_LONG) - .setAction(R.string.share, v -> showShareLinkDialog(link)); - snackbar.show(); - } - - @Override - public void showSharingMenuActionSheet(OCShare share) { - if (!this.isFinishing()) { - new NoteShareActivityShareItemActionBottomSheetDialog(this, this, share).show(); - } - } - - @Override - public void showShareExpirationSnackbar(OCShare share) { - String expirationDescription = getString( - R.string.share_expires, - SimpleDateFormat.getDateInstance().format(new Date(share.getExpirationDate())) - ); - Snackbar snackbar = Snackbar - .make(binding.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) - .setAction(R.string.dismiss, view -> { - // Clicking on empty action will dismiss snackbar - }); - final var util = BrandingUtil.of(BrandingUtil.readBrandMainColor(this), this); - util.material.themeSnackbar(snackbar); - snackbar.show(); - } - - @Override - public void showPermissionsDialog(OCShare share) { - new QuickSharingPermissionsBottomSheetDialog(this, this, share).show(); - } - - @Override - public void requestPasswordForShare(OCShare share, boolean askForPassword) { - SharePasswordDialogFragment dialog = SharePasswordDialogFragment.newInstance(share, askForPassword, this); - dialog.show(getSupportFragmentManager(), SharePasswordDialogFragment.PASSWORD_FRAGMENT); - } - - // TODO: - @Override - public void showProfileBottomSheet(Account account, String shareWith) { - } - - private boolean isInitializingShares = false; - - public void updateShareeListAdapter() { - if (isInitializingShares) { - Log_OC.d(TAG, "Shares initialization already in progress"); - return; - } - - isInitializingShares = true; - - executorService.submit(() -> { - try { - ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter(); - - if (adapter == null) { - runOnUiThread(() -> DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.could_not_retrieve_shares))); - isInitializingShares = false; - return; - } - - List remoteNotes; - if (note != null) { - remoteNotes = repository.fetchSharesFromNote(note); - } else { - remoteNotes = new ArrayList<>(); - } - - runOnUiThread(() -> { - shares.clear(); - shares.addAll(remoteNotes); - - adapter.removeAll(); - adapter.addShares(shares); - addPublicShares(adapter); - setShareWithYou(); - - isInitializingShares = false; - }); - } catch (Exception e) { - Log_OC.d(TAG, "Exception while updateShareeListAdapter: " + e); - } - }); - } - - private void addPublicShares(ShareeListAdapter adapter) { - List publicShares = new ArrayList<>(); - - if (containsNoNewPublicShare(adapter.getShares())) { - final OCShare ocShare = new OCShare(); - ocShare.setShareType(ShareType.NEW_PUBLIC_LINK); - publicShares.add(ocShare); - } else { - adapter.removeNewPublicShare(); - } - - adapter.addShares(publicShares); - } - - private void checkContactPermission() { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { - pickContactEmail(); - } else { - requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS); - } - } - - private void pickContactEmail() { - Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI); - - // FIXME: - if (intent.resolveActivity(getPackageManager()) != null) { - onContactSelectionResultLauncher.launch(intent); - } else { - DisplayUtils.showSnackMessage(this, getString(R.string.file_detail_sharing_fragment_no_contact_app_message)); - } - } - - private void handleContactResult(@NonNull Uri contactUri) { - // Define the projection to get all email addresses. - String[] projection = {ContactsContract.CommonDataKinds.Email.ADDRESS}; - - Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null); - if (cursor == null) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address as Cursor is null."); - return; - } - - if (!cursor.moveToFirst()) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address as no Email found."); - return; - } - - // The contact has only one email address, use it. - int columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS); - if (columnIndex == -1) { - DisplayUtils.showSnackMessage(this, getString(R.string.email_pick_failed)); - Log_OC.e(TAG, "Failed to pick email address."); - return; - } - - // Use the email address as needed. - // email variable contains the selected contact's email address. - String email = cursor.getString(columnIndex); - binding.searchView.post(() -> { - binding.searchView.setQuery(email, false); - binding.searchView.requestFocus(); - }); - - cursor.close(); - } - - private boolean containsNoNewPublicShare(List shares) { - for (OCShare share : shares) { - if (share.getShareType() != null && share.getShareType() == ShareType.NEW_PUBLIC_LINK) { - return false; - } - } - - return true; - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(ARG_NOTE, note); - outState.putSerializable(ARG_ACCOUNT, account); - } - - private boolean isReshareForbidden(OCShare share) { - return (share.getShareType() != null && ShareType.FEDERATED == share.getShareType()) || - capabilities != null && !capabilities.isReSharingAllowed(); - } - - @VisibleForTesting - public void search(String query) { - SearchView searchView = findViewById(R.id.searchView); - searchView.setQuery(query, true); - } - - @Override - public void advancedPermissions(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION, false); - } - - - @Override - public void sendNewEmail(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE, true); - } - - @Override - public void unShare(OCShare share) { - executorService.submit(() -> { - final var result = repository.removeShare(share, note); - - runOnUiThread(() -> { - if (result) { - ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter(); - if (adapter == null) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.email_pick_failed)); - return; - } - adapter.remove(share); - } else { - DisplayUtils.showSnackMessage(NoteShareActivity.this, getString(R.string.failed_the_remove_share)); - } - }); - }); - } - - // TODO: - @Override - public void sendLink(OCShare share) { - } - - @Override - public void addAnotherLink(OCShare share) { - createPublicShareLink(); - } - - private void modifyExistingShare(OCShare share, int screenTypePermission, boolean sendEmail) { - Bundle bundle = new Bundle(); - - bundle.putSerializable(NoteShareDetailActivity.ARG_OCSHARE, share); - bundle.putInt(NoteShareDetailActivity.ARG_SCREEN_TYPE, screenTypePermission); - bundle.putBoolean(NoteShareDetailActivity.ARG_RESHARE_SHOWN, !isReshareForbidden(share)); - bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, getExpDateShown()); - bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail); - - Intent intent = new Intent(this, NoteShareDetailActivity.class); - intent.putExtras(bundle); - resultLauncher.launch(intent); - } - - private boolean getExpDateShown() { - try { - if (capabilities == null) { - Log_OC.d(TAG, "Capabilities cannot be null"); - return false; - } - - final var majorVersionAsString = capabilities.getNextcloudMajorVersion(); - if (majorVersionAsString != null) { - final var majorVersion = Integer.parseInt(majorVersionAsString); - return majorVersion >= 18; - } - - return false; - } catch (NumberFormatException e) { - Log_OC.d(TAG, "Exception while getting expDateShown"); - return false; - } - } - - @Override - public void onQuickPermissionChanged(OCShare share, int permission) { - executorService.submit(() -> { - final var result = repository.updateSharePermission(share.getId(), permission); - runOnUiThread(() -> { - if (ApiResultKt.isSuccess(result)) { - updateShare(share); - } else if (result instanceof ApiResult.Error error) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - }); - } - - private void updateShare(OCShare share) { - if (note == null) { - Log_OC.e(TAG, "note is null, cannot update share"); - return; - } - - executorService.submit(() -> { - try { - final var updatedShares = repository.fetchSharesFromNote(note); - - runOnUiThread(() -> { - if (binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - OCShare updatedShare = null; - for (int i=0;i requestContactPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - pickContactEmail(); - } else { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.note_share_activity_contact_no_permission), Snackbar.LENGTH_LONG) - .show(); - } - }); - - private final ActivityResultLauncher onContactSelectionResultLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent intent = result.getData(); - if (intent == null) { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.email_pick_failed), Snackbar.LENGTH_LONG) - .show(); - return; - } - - Uri contactUri = intent.getData(); - if (contactUri == null) { - BrandedSnackbar.make(binding.getRoot(), getString(R.string.email_pick_failed), Snackbar.LENGTH_LONG) - .show(); - return; - } - - handleContactResult(contactUri); - - } - }); - - @Override - public void applyBrand(int color) { - final var util = BrandingUtil.of(color, this); - util.platform.themeStatusBar(this); - util.material.themeToolbar(binding.toolbar); - util.notes.themeContentSearchView(binding.searchView); - util.platform.colorImageView(binding.searchViewIcon, ColorRole.ON_SURFACE_VARIANT); - util.platform.colorImageView(binding.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT); - util.platform.colorCircularProgressBar(binding.loadingLayoutIndicator, ColorRole.PRIMARY); - util.platform.themeHorizontalProgressBar(binding.progressBar); - util.platform.colorViewBackground(getWindow().getDecorView()); - util.platform.colorViewBackground(binding.getRoot()); - util.material.colorMaterialButtonPrimaryOutlined(binding.btnShareButton); - util.platform.colorTextView(binding.title, ColorRole.ON_SURFACE); - util.platform.colorTextView(binding.fileName, ColorRole.ON_SURFACE_VARIANT); - util.notes.themeSearchCardView(binding.searchCardWrapper); - } - - @Override - protected void onDestroy() { - executorService.shutdown(); - super.onDestroy(); - } - - @Override - public void shareFileViaPublicShare(@Nullable Note note, @Nullable String password) { - if (note == null || password == null) { - Log_OC.d(TAG, "note or password is null, cannot create a public share"); - return; - } - - executorService.submit(() -> { - final var result = repository.addShare( - note, - ShareType.PUBLIC_LINK, - "", - "false", - password, - repository.getCapabilities().getDefaultPermission() - ); - - runOnUiThread(() -> { - if (result instanceof ApiResult.Success> successResponse && - binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { - adapter.addShare(CreateShareResponseExtensionsKt.toOCShare(successResponse.getData().ocs.data)); - } else if (result instanceof ApiResult.Error error) { - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - }); - } - - @Override - public void setPasswordToShare(@NotNull OCShare share, @Nullable String password) { - if (password == null) { - Log_OC.d(TAG, "password is null, cannot update a public share"); - return; - } - - executorService.submit(() -> { - { - boolean isDownloadAndAllowsSyncEnabled = repository.isAllowDownloadAndSync(share); - - String attributes = UpdateShareRequest.Companion.createAttributes( - repository.getCapabilities(), - isDownloadAndAllowsSyncEnabled, - share.getShareType() - ); - - final var requestBody = new UpdateShareRequest( - share.getPermissions(), - password, - share.getNote(), - share.getLabel(), - DateUtil.INSTANCE.getExpirationDate(share.getExpirationDate()), - Boolean.toString(share.isHideFileDownload()), - attributes - ); - final var result = repository.updateShare(share.getId(), requestBody); - - runOnUiThread(() -> { - if (ApiResultKt.isSuccess(result)) { - final var intent = new Intent(NoteShareActivity.this, MainActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - NoteShareActivity.this.startActivity(intent); - } else if (ApiResultKt.isError(result)) { - ApiResult.Error error = (ApiResult.Error) result; - DisplayUtils.showSnackMessage(NoteShareActivity.this, error.getMessage()); - } - }); - } - }); - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt new file mode 100644 index 000000000..4d7e4a6f4 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareFragment.kt @@ -0,0 +1,887 @@ +package it.niedermann.owncloud.notes.share + +import android.Manifest +import android.app.Activity +import android.app.Activity.RESULT_OK +import android.app.SearchManager +import android.content.Context.INPUT_METHOD_SERVICE +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Bundle +import android.provider.ContactsContract +import android.text.InputType +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.SearchView +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import androidx.core.view.isVisible +import androidx.core.view.size +import androidx.fragment.app.DialogFragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.BrandedFragment +import it.niedermann.owncloud.notes.branding.BrandedSnackbar +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ActivityNoteShareBinding +import it.niedermann.owncloud.notes.main.MainActivity +import it.niedermann.owncloud.notes.persistence.ApiResult +import it.niedermann.owncloud.notes.persistence.entity.Account +import it.niedermann.owncloud.notes.persistence.entity.Note +import it.niedermann.owncloud.notes.persistence.isError +import it.niedermann.owncloud.notes.persistence.isSuccess +import it.niedermann.owncloud.notes.share.adapter.ShareeListAdapter +import it.niedermann.owncloud.notes.share.adapter.SuggestionAdapter +import it.niedermann.owncloud.notes.share.dialog.NoteShareActivityShareItemActionBottomSheetDialog +import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog +import it.niedermann.owncloud.notes.share.dialog.QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions +import it.niedermann.owncloud.notes.share.dialog.ShareLinkToDialog.Companion.newInstance +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment.Companion.newInstance +import it.niedermann.owncloud.notes.share.dialog.SharePasswordDialogFragment.SharePasswordDialogListener +import it.niedermann.owncloud.notes.share.helper.AvatarLoader +import it.niedermann.owncloud.notes.share.helper.UsersAndGroupsSearchProvider +import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction +import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener +import it.niedermann.owncloud.notes.share.model.CreateShareResponse +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest.Companion.createAttributes +import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig.reset +import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig.searchOnlyUsers +import it.niedermann.owncloud.notes.share.model.toOCShare +import it.niedermann.owncloud.notes.share.repository.ShareRepository +import it.niedermann.owncloud.notes.shared.model.Capabilities +import it.niedermann.owncloud.notes.shared.model.OcsResponse +import it.niedermann.owncloud.notes.shared.util.DisplayUtils +import it.niedermann.owncloud.notes.shared.util.ShareUtil +import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil.copyToClipboard +import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument +import it.niedermann.owncloud.notes.util.DateUtil.getExpirationDate +import java.text.SimpleDateFormat +import java.util.Collections +import java.util.Date +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import kotlin.text.toInt + +class NoteShareFragment : BrandedFragment(), ShareeListAdapterListener, NoteShareItemAction, + QuickPermissionSharingBottomSheetActions, SharePasswordDialogListener { + private var executorService: ScheduledExecutorService? = null + private var future: Future<*>? = null + private var binding: ActivityNoteShareBinding? = null + private var note: Note? = null + private var account: Account? = null + private var repository: ShareRepository? = null + private var capabilities: Capabilities? = null + private var resultLauncher: ActivityResultLauncher? = null + private val shares: MutableList = + Collections.synchronizedList(ArrayList()) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + executorService = Executors.newSingleThreadScheduledExecutor() + binding = ActivityNoteShareBinding.inflate(layoutInflater) + initializeArguments() + return binding!!.getRoot() + } + + private fun initializeArguments() { + val bundler = arguments + note = bundler.getSerializableArgument(ARG_NOTE, Note::class.java) + account = bundler.getSerializableArgument(ARG_ACCOUNT, Account::class.java) + + requireNotNull(note) { "Note cannot be null" } + + requireNotNull(account) { "Account cannot be null" } + + executorService!!.submit { + try { + val ssoAcc = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) + repository = ShareRepository(requireContext(), ssoAcc) + capabilities = repository!!.getCapabilities() + repository!!.fetchSharesForNotesAndSaveShareEntities() + + activity?.runOnUiThread { + binding!!.searchContainer.visibility = View.VISIBLE + binding!!.sharesList.visibility = View.VISIBLE + binding!!.sharesList.setAdapter( + ShareeListAdapter( + activity, + ArrayList(), + this, + account + ) + ) + binding!!.sharesList.setLayoutManager(LinearLayoutManager(requireContext())) + binding!!.pickContactEmailBtn.setOnClickListener { checkContactPermission() } + binding!!.btnShareButton.setOnClickListener { + ShareUtil.openShareDialog( + requireContext(), + note!!.title, + note!!.content + ) + } + + if (note!!.readonly) { + setupReadOnlySearchView() + } else { + setupSearchView() + } + + updateShareeListAdapter() + binding!!.loadingLayout.visibility = View.GONE + } + } catch (e: Exception) { + Log_OC.e(TAG, "Exception at NoteShareActivity.init: $e") + } + } + } + + public override fun onStart() { + super.onStart() + + // note cannot be encrypted - logic ported from files app + searchOnlyUsers = false + } + + public override fun onStop() { + super.onStop() + reset() + } + + private fun disableSearchView(view: View) { + view.setEnabled(false) + + if (view is ViewGroup) { + for (i in 0..(androidx.appcompat.R.id.search_close_btn) + closeButton.setOnClickListener { + binding!!.progressBar.visibility = View.GONE + binding!!.searchView.setQuery("", false) + } + + binding!!.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + // return true to prevent the query from being processed; + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + if (newText.isNotEmpty()) { + binding!!.progressBar.visibility = View.VISIBLE + + // Cancel the previous task if it's still running + if (future != null && !future!!.isDone) { + future!!.cancel(true) + } + + // Schedule a new task with a delay + future = executorService!!.schedule(Runnable schedule@{ + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return@schedule + } + val isFederationShareAllowed = capabilities!!.federationShare + try { + val cursor = + provider.searchForUsersOrGroups(newText, isFederationShareAllowed) + + if (cursor == null || cursor.count == 0) { + return@schedule + } + + activity?.runOnUiThread { + if (binding!!.searchView.isVisible) { + suggestionAdapter.swapCursor(cursor) + } + binding!!.progressBar.visibility = View.GONE + } + } catch (e: Exception) { + Log_OC.d(TAG, "Exception setupSearchView.onQueryTextChange: $e") + activity?.runOnUiThread { binding!!.progressBar.visibility = View.GONE } + } + }, SEARCH_DELAY_MS, TimeUnit.MILLISECONDS) + } else { + binding!!.progressBar.visibility = View.GONE + suggestionAdapter.swapCursor(null) + } + return false + } + }) + + binding!!.searchView.setOnQueryTextFocusChangeListener { _: View?, hasFocus: Boolean -> + if (hasFocus) { + val imm = context?.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput( + binding!!.searchView.findFocus(), + InputMethodManager.SHOW_IMPLICIT + ) + } + } + + binding!!.searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener { + override fun onSuggestionSelect(position: Int): Boolean { + return false + } + + override fun onSuggestionClick(position: Int): Boolean { + val cursor = suggestionAdapter.cursor + if (cursor != null && cursor.moveToPosition(position)) { + val suggestion = + cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)) + binding!!.searchView.setQuery(suggestion, false) + + val shareWith = + cursor.getString(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_WITH)) + val shareType = + cursor.getInt(cursor.getColumnIndexOrThrow(UsersAndGroupsSearchProvider.SHARE_TYPE)) + navigateNoteShareDetail(shareWith, shareType) + } + return true + } + }) + } + + private fun navigateNoteShareDetail(shareWith: String?, shareType: Int) { + val bundle = Bundle() + + bundle.putSerializable(NoteShareDetailActivity.ARG_NOTE, note) + bundle.putString(NoteShareDetailActivity.ARG_SHAREE_NAME, shareWith) + bundle.putInt(NoteShareDetailActivity.ARG_SHARE_TYPE, shareType) + + val intent = Intent(context, NoteShareDetailActivity::class.java) + intent.putExtras(bundle) + resultLauncher!!.launch(intent) + } + + private fun accountOwnsFile(): Boolean { + if (shares.isEmpty()) { + return true + } + + val share = shares[0] + val ownerDisplayName = share.ownerDisplayName + return TextUtils.isEmpty(ownerDisplayName) || account!!.accountName + .split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].equals( + ownerDisplayName, + ignoreCase = true + ) + } + + private fun setShareWithYou() { + if (accountOwnsFile()) { + binding!!.sharedWithYouContainer.visibility = View.GONE + } else { + if (shares.isEmpty()) { + return + } + + val share = shares[0] + + binding!!.sharedWithYouUsername.text = String.format( + getString(R.string.note_share_activity_shared_with_you), + share.ownerDisplayName + ) + AvatarLoader.load(requireContext(), binding!!.sharedWithYouAvatar, account!!) + binding!!.sharedWithYouAvatar.setVisibility(View.VISIBLE) + + val description = share.note + + if (!TextUtils.isEmpty(description)) { + binding!!.sharedWithYouNote.setText(description) + binding!!.sharedWithYouNoteContainer.visibility = View.VISIBLE + } else { + binding!!.sharedWithYouNoteContainer.visibility = View.GONE + } + } + } + + override fun copyInternalLink() { + if (account == null) { + DisplayUtils.showSnackMessage( + view, + getString(R.string.note_share_activity_could_not_retrieve_url) + ) + return + } + + val link = createInternalLink() + showShareLinkDialog(link) + } + + override fun createPublicShareLink() { + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return + } + + if (capabilities!!.publicPasswordEnforced || capabilities!!.askForOptionalPassword) { + // password enforced by server, request to the user before trying to create + requestPasswordForShareViaLink(true, capabilities!!.askForOptionalPassword) + } else { + executorService!!.submit { + val result: ApiResult?> = + repository!!.addShare(note!!, ShareType.PUBLIC_LINK, "") + activity?.runOnUiThread { + if (result is ApiResult.Success?> && binding!!.sharesList.adapter is ShareeListAdapter) { + DisplayUtils.showSnackMessage(view, result.message) + note!!.setIsShared(true) + repository!!.updateNote(note!!) + val adapter = binding!!.sharesList.adapter as ShareeListAdapter + adapter.addShare(result.data!!.ocs.data!!.toOCShare()) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(view, result.message) + } + } + } + } + } + + fun requestPasswordForShareViaLink(createShare: Boolean, askForPassword: Boolean) { + val dialog = newInstance(note, createShare, askForPassword, this) + dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) + } + + // TODO: + override fun createSecureFileDrop() { + } + + private fun showShareLinkDialog(link: String?) { + val intentToShareLink = Intent(Intent.ACTION_SEND) + + intentToShareLink.putExtra(Intent.EXTRA_TEXT, link) + intentToShareLink.setType("text/plain") + intentToShareLink.putExtra( + Intent.EXTRA_SUBJECT, + getString(R.string.note_share_activity_subject_shared_with_you, note!!.title) + ) + + val packagesToExclude = arrayOf(context?.packageName) + val chooserDialog: DialogFragment = newInstance(intentToShareLink, *packagesToExclude) + chooserDialog.show(parentFragmentManager, FTAG_CHOOSER_DIALOG) + } + + private fun createInternalLink(): String { + val baseUri = account!!.url.toUri() + return baseUri.toString() + "/index.php/f/" + note!!.remoteId + } + + override fun copyLink(share: OCShare) { + if (!note!!.isShared) { + return + } + + if (TextUtils.isEmpty(share.shareLink)) { + copyAndShareFileLink(share.shareLink) + } else { + copyToClipboard(requireContext(), share.shareLink) + } + } + + private fun copyAndShareFileLink(link: String?) { + copyToClipboard(requireContext(), link, false) + val snackbar = Snackbar + .make( + requireView(), + R.string.clipboard_text_copied, + Snackbar.LENGTH_LONG + ) + .setAction( + R.string.share + ) { showShareLinkDialog(link) } + snackbar.show() + } + + override fun showSharingMenuActionSheet(share: OCShare?) { + NoteShareActivityShareItemActionBottomSheetDialog(activity, this, share).show() + } + + override fun showShareExpirationSnackbar(share: OCShare) { + val expirationDescription = getString( + R.string.share_expires, + SimpleDateFormat.getDateInstance().format(Date(share.expirationDate)) + ) + val snackbar = Snackbar + .make(binding!!.getRoot(), expirationDescription, Snackbar.LENGTH_LONG) + .setAction(R.string.dismiss) { } + val util = BrandingUtil.of(BrandingUtil.readBrandMainColor(requireContext()), requireContext()) + util.material.themeSnackbar(snackbar) + snackbar.show() + } + + override fun showPermissionsDialog(share: OCShare?) { + QuickSharingPermissionsBottomSheetDialog(activity, this, share).show() + } + + override fun requestPasswordForShare(share: OCShare?, askForPassword: Boolean) { + val dialog = newInstance(share, askForPassword, this) + dialog.show(parentFragmentManager, SharePasswordDialogFragment.PASSWORD_FRAGMENT) + } + + // TODO: + override fun showProfileBottomSheet(account: Account?, shareWith: String?) { + } + + private var isInitializingShares = false + + fun updateShareeListAdapter() { + if (isInitializingShares) { + Log_OC.d(TAG, "Shares initialization already in progress") + return + } + + isInitializingShares = true + + executorService!!.submit(Runnable submit@{ + try { + val adapter = binding!!.sharesList.adapter as ShareeListAdapter? + + if (adapter == null) { + activity?.runOnUiThread { + DisplayUtils.showSnackMessage( + view, + getString(R.string.could_not_retrieve_shares) + ) + } + isInitializingShares = false + return@submit + } + val remoteNotes: List = if (note != null) { + repository!!.fetchSharesFromNote(note!!) + } else { + listOf() + } + + activity?.runOnUiThread { + shares.clear() + shares.addAll(remoteNotes) + + adapter.removeAll() + adapter.addShares(shares) + addPublicShares(adapter) + setShareWithYou() + isInitializingShares = false + } + } catch (e: Exception) { + Log_OC.d(TAG, "Exception while updateShareeListAdapter: $e") + } + }) + } + + private fun addPublicShares(adapter: ShareeListAdapter) { + val publicShares: MutableList = ArrayList() + + if (containsNoNewPublicShare(adapter.shares)) { + val ocShare = OCShare() + ocShare.shareType = ShareType.NEW_PUBLIC_LINK + publicShares.add(ocShare) + } else { + adapter.removeNewPublicShare() + } + + adapter.addShares(publicShares) + } + + private fun checkContactPermission() { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + ) { + pickContactEmail() + } else { + requestContactPermissionLauncher.launch(Manifest.permission.READ_CONTACTS) + } + } + + private fun pickContactEmail() { + val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Email.CONTENT_URI) + + // FIXME: + if (intent.resolveActivity(requireContext().packageManager) != null) { + onContactSelectionResultLauncher.launch(intent) + } else { + DisplayUtils.showSnackMessage( + view, + getString(R.string.file_detail_sharing_fragment_no_contact_app_message) + ) + } + } + + private fun handleContactResult(contactUri: Uri) { + // Define the projection to get all email addresses. + val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS) + + val cursor = context?.contentResolver?.query(contactUri, projection, null, null, null) + if (cursor == null) { + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address as Cursor is null.") + return + } + + if (!cursor.moveToFirst()) { + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address as no Email found.") + return + } + + // The contact has only one email address, use it. + val columnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + if (columnIndex == -1) { + DisplayUtils.showSnackMessage(view, getString(R.string.email_pick_failed)) + Log_OC.e(TAG, "Failed to pick email address.") + return + } + + // Use the email address as needed. + // email variable contains the selected contact's email address. + val email = cursor.getString(columnIndex) + binding!!.searchView.post { + binding!!.searchView.setQuery(email, false) + binding!!.searchView.requestFocus() + } + + cursor.close() + } + + private fun containsNoNewPublicShare(shares: MutableList): Boolean { + for (share in shares) { + if (share.shareType != null && share.shareType == ShareType.NEW_PUBLIC_LINK) { + return false + } + } + + return true + } + + private fun isReshareForbidden(share: OCShare): Boolean { + return (share.shareType != null && ShareType.FEDERATED == share.shareType) || + capabilities != null && !capabilities!!.isReSharingAllowed + } + + @VisibleForTesting + fun search(query: String?) { + val searchView = binding?.searchView + searchView?.setQuery(query, true) + } + + override fun advancedPermissions(share: OCShare) { + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION, false) + } + + + override fun sendNewEmail(share: OCShare) { + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE, true) + } + + override fun unShare(share: OCShare) { + executorService!!.submit(Runnable { + val result = repository!!.removeShare(share, note!!) + activity?.runOnUiThread(Runnable { + if (result) { + val adapter = binding!!.sharesList.adapter as ShareeListAdapter? + if (adapter == null) { + DisplayUtils.showSnackMessage( + view, + getString(R.string.email_pick_failed) + ) + return@Runnable + } + adapter.remove(share) + } else { + DisplayUtils.showSnackMessage( + view, + getString(R.string.failed_the_remove_share) + ) + } + }) + }) + } + + // TODO: + override fun sendLink(share: OCShare?) { + } + + override fun addAnotherLink(share: OCShare?) { + createPublicShareLink() + } + + private fun modifyExistingShare(share: OCShare, screenTypePermission: Int, sendEmail: Boolean) { + val bundle = Bundle() + + bundle.putSerializable(NoteShareDetailActivity.ARG_OCSHARE, share) + bundle.putInt(NoteShareDetailActivity.ARG_SCREEN_TYPE, screenTypePermission) + bundle.putBoolean(NoteShareDetailActivity.ARG_RESHARE_SHOWN, !isReshareForbidden(share)) + bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, this.expDateShown) + bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail) + + val intent = Intent(context, NoteShareDetailActivity::class.java) + intent.putExtras(bundle) + resultLauncher!!.launch(intent) + } + + private val expDateShown: Boolean + get() { + try { + if (capabilities == null) { + Log_OC.d(TAG, "Capabilities cannot be null") + return false + } + + val majorVersionAsString = capabilities!!.nextcloudMajorVersion + if (majorVersionAsString != null) { + val majorVersion = majorVersionAsString.toInt() + return majorVersion >= 18 + } + + return false + } catch (_: NumberFormatException) { + Log_OC.d(TAG, "Exception while getting expDateShown") + return false + } + } + + override fun onQuickPermissionChanged(share: OCShare, permission: Int) { + executorService!!.submit { + val result: ApiResult?> = + repository!!.updateSharePermission(share.id, permission) + activity?.runOnUiThread { + if (result.isSuccess()) { + updateShare(share) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(view, result.message) + } + } + } + } + + private fun updateShare(share: OCShare) { + if (note == null) { + Log_OC.e(TAG, "note is null, cannot update share") + return + } + + executorService!!.submit(Runnable { + try { + val updatedShares: List = repository!!.fetchSharesFromNote(note!!) + + activity?.runOnUiThread(Runnable { + if (binding!!.sharesList.adapter is ShareeListAdapter) { + var updatedShare: OCShare? = null + for (i in updatedShares.indices) { + if (updatedShares[i]!!.id == share.id) { + updatedShare = updatedShares[i] + break + } + } + + if (updatedShare == null) { + return@Runnable + } + + val adapter = binding!!.sharesList.adapter as ShareeListAdapter + adapter.updateShare(updatedShare) + } + }) + } catch (e: Exception) { + Log_OC.d(TAG, "Exception while refreshing share info: $e") + } + }) + } + + private val requestContactPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean? -> + if (isGranted == true) { + pickContactEmail() + } else { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.note_share_activity_contact_no_permission), + Snackbar.LENGTH_LONG + ) + .show() + } + } + + private val onContactSelectionResultLauncher = + registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ActivityResultCallback { result: ActivityResult? -> + if (result!!.resultCode == RESULT_OK) { + val intent = result.data + if (intent == null) { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.email_pick_failed), + Snackbar.LENGTH_LONG + ) + .show() + return@ActivityResultCallback + } + + val contactUri = intent.data + if (contactUri == null) { + BrandedSnackbar.make( + binding!!.getRoot(), + getString(R.string.email_pick_failed), + Snackbar.LENGTH_LONG + ) + .show() + return@ActivityResultCallback + } + + handleContactResult(contactUri) + } + }) + + override fun applyBrand(color: Int) { + val util = BrandingUtil.of(color, requireContext()) + util.notes.themeContentSearchView(binding!!.searchView) + util.platform.colorImageView(binding!!.searchViewIcon, ColorRole.ON_SURFACE_VARIANT) + util.platform.colorImageView(binding!!.pickContactEmailBtn, ColorRole.ON_SURFACE_VARIANT) + util.platform.colorCircularProgressBar(binding!!.loadingLayoutIndicator, ColorRole.PRIMARY) + util.platform.themeHorizontalProgressBar(binding!!.progressBar) + util.platform.colorViewBackground(binding!!.getRoot()) + util.material.colorMaterialButtonPrimaryOutlined(binding!!.btnShareButton) + util.notes.themeSearchCardView(binding!!.searchCardWrapper) + } + + override fun onDestroy() { + executorService!!.shutdown() + super.onDestroy() + } + + override fun shareFileViaPublicShare(note: Note?, password: String?) { + if (note == null || password == null) { + Log_OC.d(TAG, "note or password is null, cannot create a public share") + return + } + + executorService!!.submit { + val result: ApiResult?> = repository!!.addShare( + note, + ShareType.PUBLIC_LINK, + "", + "false", + password, + repository!!.getCapabilities().defaultPermission + ) + activity?.runOnUiThread { + if (result is ApiResult.Success?> && + binding!!.sharesList.adapter is ShareeListAdapter + ) { + val adapter = binding!!.sharesList.adapter as ShareeListAdapter + adapter.addShare(result.data!!.ocs.data!!.toOCShare()) + } else if (result is ApiResult.Error) { + DisplayUtils.showSnackMessage(view, result.message) + } + } + } + } + + override fun setPasswordToShare(share: OCShare, password: String?) { + if (password == null) { + Log_OC.d(TAG, "password is null, cannot update a public share") + return + } + + executorService!!.submit { + run { + val isDownloadAndAllowsSyncEnabled = repository!!.isAllowDownloadAndSync(share) + val attributes = createAttributes( + repository!!.getCapabilities(), + isDownloadAndAllowsSyncEnabled, + share.shareType + ) + + val requestBody = UpdateShareRequest( + share.permissions, + password, + share.note, + share.label, + getExpirationDate(share.expirationDate), + share.isHideFileDownload.toString(), + attributes + ) + val result: ApiResult?> = + repository!!.updateShare(share.id, requestBody) + activity?.runOnUiThread { + if (result.isSuccess()) { + val intent = Intent(context, MainActivity::class.java) + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity?.startActivity(intent) + } else if (result.isError()) { + val error = result as ApiResult.Error + DisplayUtils.showSnackMessage(view, error.message) + } + } + } + } + } + + companion object { + private const val TAG = "NoteShareFragment" + const val ARG_NOTE: String = "NOTE" + const val ARG_ACCOUNT: String = "ACCOUNT" + const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" + + private const val SEARCH_DELAY_MS: Long = 500 + + fun newInstance(note: Note, account: Account) : NoteShareFragment { + val f = NoteShareFragment() + + val bundle = Bundle() + bundle.putSerializable(ARG_NOTE, note) + bundle.putSerializable(ARG_ACCOUNT, account) + + f.arguments = bundle + return f + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java index 0f1a4084f..5e24d0303 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/NoteClickListener.java @@ -13,5 +13,5 @@ public interface NoteClickListener { void onNoteFavoriteClick(int position, View v); - void openShare(int position); + void openDetails(int position); } \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt index 0e46f8210..a4ef95a3e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt @@ -6,7 +6,6 @@ */ package it.niedermann.owncloud.notes.shared.util.clipboard -import android.app.Activity import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -24,26 +23,26 @@ object ClipboardUtil { @JvmStatic @JvmOverloads @Suppress("TooGenericExceptionCaught") - fun copyToClipboard(activity: Activity, text: String?, showToast: Boolean = true) { + fun copyToClipboard(context: Context, text: String?, showToast: Boolean = true) { if (!TextUtils.isEmpty(text)) { try { val clip = ClipData.newPlainText( - activity.getString( + context.getString( R.string.clipboard_label, - activity.getString(R.string.app_name) + context.getString(R.string.app_name) ), text ) - (activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip) + (context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip) if (showToast) { - Toast.makeText(activity, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - Toast.makeText(activity, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_unexpected_error, Toast.LENGTH_SHORT).show() Log_OC.e(TAG, "Exception caught while copying to clipboard", e) } } else { - Toast.makeText(activity, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.clipboard_no_text_to_copy, Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/res/drawable/ic_activity.xml b/app/src/main/res/drawable/ic_activity.xml new file mode 100644 index 000000000..a9505658c --- /dev/null +++ b/app/src/main/res/drawable/ic_activity.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/nav_activity.xml b/app/src/main/res/drawable/nav_activity.xml new file mode 100644 index 000000000..32809f5d7 --- /dev/null +++ b/app/src/main/res/drawable/nav_activity.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/layout/activity_item_details.xml b/app/src/main/res/layout/activity_item_details.xml new file mode 100644 index 000000000..7569eafa2 --- /dev/null +++ b/app/src/main/res/layout/activity_item_details.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_note_share.xml b/app/src/main/res/layout/activity_note_share.xml index ffeb8cf6b..121dab920 100644 --- a/app/src/main/res/layout/activity_note_share.xml +++ b/app/src/main/res/layout/activity_note_share.xml @@ -41,43 +41,6 @@ - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml index 2191dba0c..21425b0a6 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml @@ -126,20 +126,18 @@ tools:text="27.11." /> + app:iconTint="@color/defaultBrand" /> diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml index 363a4306e..23239131e 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid_only_title.xml @@ -115,20 +115,18 @@ tools:text="27.11." /> + app:iconTint="@color/defaultBrand" /> diff --git a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml index ac3482c03..995a859eb 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_with_excerpt.xml @@ -169,19 +169,17 @@ tools:text="@tools:sample/lorem/random" /> + app:iconTint="@color/defaultBrand" /> diff --git a/app/src/main/res/menu/menu_list_context_multiple.xml b/app/src/main/res/menu/menu_list_context_multiple.xml index 1a8491212..5f79dccbc 100644 --- a/app/src/main/res/menu/menu_list_context_multiple.xml +++ b/app/src/main/res/menu/menu_list_context_multiple.xml @@ -15,6 +15,13 @@ android:title="@string/menu_change_category" app:showAsAction="ifRoom" /> + + @color/material_dynamic_secondary20 @color/transparent + + #49454F + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f3a39be8a..c5649baf1 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -59,4 +59,9 @@ @color/high_emphasis_text @color/transparent + + #CAC4D0 + @color/secondary_text_color + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b42e9c41b..d842dc8ae 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -82,4 +82,6 @@ 56dp 60dp 24dp + + @dimen/spacer_2x diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80d4284fc..d0055f25d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -537,4 +537,14 @@ Share expires on %1$s Dismiss Account not found! + + Details + Activities + + Note.md + Favorite + 389 KB +  • + 12:23:45 +