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
+