diff --git a/app/build.gradle b/app/build.gradle
index baed71e00..06099f31c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -95,6 +95,15 @@ ext {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
+ implementation 'com.google.guava:guava:31.1-android'
+ implementation ('commons-httpclient:commons-httpclient:3.1') {
+ exclude group: 'commons-logging', module: 'commons-logging'
+ }
+
+ implementation("com.github.nextcloud:android-library:2.19.0") {
+ exclude group: 'org.ogce', module: 'xpp3'
+ }
+
// Nextcloud SSO
implementation 'com.github.nextcloud.android-common:ui:48ed8e86d9'
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f4427996f..6a43f4dba 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,7 +11,8 @@
-
+
+
@@ -47,10 +48,22 @@
android:value=".android.activity.NotesListViewActivity" />
+
+
+
+
+
+
dismiss());
final var adapter = new AccountSwitcherAdapter((localAccount -> {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
index b3119fdd5..d128e07ef 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
@@ -14,13 +14,9 @@
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
-import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
@@ -34,12 +30,7 @@ public AccountSwitcherViewHolder(@NonNull View itemView) {
public void bind(@NonNull Account localAccount, @NonNull Consumer onAccountClick) {
binding.accountName.setText(localAccount.getDisplayName());
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
- Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
- .placeholder(R.drawable.ic_account_circle_grey_24dp)
- .error(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.accountItemAvatar);
+ AvatarLoader.INSTANCE.load(itemView.getContext(), binding.accountItemAvatar, localAccount);
itemView.setOnClickListener((v) -> onAccountClick.accept(localAccount));
binding.accountContextMenu.setVisibility(View.GONE);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialog.kt b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialog.kt
new file mode 100644
index 000000000..0aed84b85
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialog.kt
@@ -0,0 +1,15 @@
+package it.niedermann.owncloud.notes.branding
+
+import android.content.Context
+import androidx.annotation.ColorInt
+import com.google.android.material.bottomsheet.BottomSheetDialog
+
+abstract class BrandedBottomSheetDialog(context: Context) : BottomSheetDialog(context), Branded {
+
+ override fun onStart() {
+ super.onStart()
+
+ @ColorInt val color = BrandingUtil.readBrandMainColor(context)
+ applyBrand(color)
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedViewHolder.kt
new file mode 100644
index 000000000..6804bfae7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedViewHolder.kt
@@ -0,0 +1,13 @@
+package it.niedermann.owncloud.notes.branding
+
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.recyclerview.widget.RecyclerView
+
+abstract class BrandedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), Branded {
+
+ fun bindBranding() {
+ @ColorInt val color = BrandingUtil.readBrandMainColor(itemView.context)
+ applyBrand(color)
+ }
+}
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 28c38152a..54f23cf50 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
@@ -122,7 +122,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
if (content == null) {
throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing.");
} else {
- note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0);
+ note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0, false, false);
requireActivity().runOnUiThread(() -> onNoteLoaded(note));
requireActivity().invalidateOptionsMenu();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
index cb59994eb..ee24c2820 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
@@ -302,7 +302,7 @@ private void launchNewNote() {
if (content == null) {
content = "";
}
- final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
+ final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null, false, false);
fragment = getNewNoteFragment(newNote);
replaceFragment();
}
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 30d3e0374..2d08ffc91 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
@@ -99,6 +99,7 @@
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.helper.AvatarLoader;
import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
@@ -517,7 +518,7 @@ public void onError(@NonNull Throwable t) {
public void onSelectionChanged() {
super.onSelectionChanged();
if (tracker.hasSelection() && mActionMode == null) {
- mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this, coordinatorLayout, binding.activityNotesListView.fabCreate, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
+ mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this,MainActivity.this, coordinatorLayout, binding.activityNotesListView.fabCreate, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
}
if (mActionMode != null) {
if (tracker.hasSelection()) {
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 9996562ad..929ad1319 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
@@ -7,6 +7,8 @@
package it.niedermann.owncloud.notes.main;
import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
@@ -31,6 +33,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.shared.util.ShareUtil;
public class MultiSelectedActionModeCallback implements Callback {
@@ -53,8 +56,11 @@ public class MultiSelectedActionModeCallback implements Callback {
private final SelectionTracker tracker;
@NonNull
private final FragmentManager fragmentManager;
+ @NonNull
+ private final MainActivity mainActivity;
public MultiSelectedActionModeCallback(
+ @NonNull MainActivity mainActivity,
@NonNull Context context,
@NonNull View view,
@NonNull View anchorView,
@@ -63,6 +69,7 @@ public MultiSelectedActionModeCallback(
boolean canMoveNoteToAnotherAccounts,
@NonNull SelectionTracker tracker,
@NonNull FragmentManager fragmentManager) {
+ this.mainActivity = mainActivity;
this.context = context;
this.view = view;
this.anchorView = anchorView;
@@ -153,16 +160,26 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
}
tracker.clearSelection();
- executor.submit(() -> {
- if (selection.size() == 1) {
- final var note = mainViewModel.getFullNote(selection.get(0));
- ShareUtil.openShareDialog(context, note.getTitle(), note.getContent());
- } else {
- ShareUtil.openShareDialog(context,
- context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
- mainViewModel.collectNoteContents(selection));
- }
- });
+ 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));
+ Bundle bundle = new Bundle();
+ bundle.putSerializable(NoteShareActivity.ARG_NOTE, note);
+ bundle.putSerializable(NoteShareActivity.ARG_ACCOUNT, account);
+ Intent intent = new Intent(mainActivity, NoteShareActivity.class);
+ intent.putExtras(bundle);
+ mainActivity.startActivity(intent);
+ }});
+ });
+ } else {
+ ShareUtil.openShareDialog(context,
+ context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
+ mainViewModel.collectNoteContents(selection));
+ }
+
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();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
index 49d7b0734..b2de7704d 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java
@@ -18,14 +18,11 @@
import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
@@ -43,11 +40,7 @@ public void bind(
) {
binding.accountName.setText(localAccount.getUserName());
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
- Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
- .error(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.accountItemAvatar);
+ AvatarLoader.INSTANCE.load(itemView.getContext(), binding.accountItemAvatar, localAccount);
itemView.setOnClickListener((v) -> callback.onSelect(localAccount));
binding.accountContextMenu.setVisibility(VISIBLE);
binding.accountContextMenu.setOnClickListener((v) -> {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
index c6268f6f0..e183b4c18 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
@@ -18,6 +18,7 @@
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;
+import com.google.gson.Strictness;
import com.nextcloud.android.sso.api.NextcloudAPI;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
@@ -29,8 +30,10 @@
import it.niedermann.owncloud.notes.persistence.sync.FilesAPI;
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
+import it.niedermann.owncloud.notes.persistence.sync.ShareAPI;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import okhttp3.ResponseBody;
import retrofit2.NextcloudRetrofitApiBuilder;
import retrofit2.Retrofit;
@@ -47,12 +50,14 @@ public class ApiProvider {
private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/";
+ private static final String API_ENDPOINT_FILES_SHARING ="/ocs/v2.php/apps/files_sharing/api/v1/";
private static final Map API_CACHE = new ConcurrentHashMap<>();
private static final Map API_CACHE_OCS = new ConcurrentHashMap<>();
private static final Map API_CACHE_NOTES = new ConcurrentHashMap<>();
private static final Map API_CACHE_FILES = new ConcurrentHashMap<>();
+ private static final Map API_CACHE_FILES_SHARING = new ConcurrentHashMap<>();
public static ApiProvider getInstance() {
@@ -96,6 +101,15 @@ public synchronized FilesAPI getFilesAPI(@NonNull Context context, @NonNull Sing
return filesAPI;
}
+ public synchronized ShareAPI getShareAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ if (API_CACHE_FILES_SHARING.containsKey(ssoAccount.name)) {
+ return API_CACHE_FILES_SHARING.get(ssoAccount.name);
+ }
+ final var shareAPI = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_FILES_SHARING).create(ShareAPI.class);
+ API_CACHE_FILES_SHARING.put(ssoAccount.name, shareAPI);
+ return shareAPI;
+ }
+
private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
if (API_CACHE.containsKey(ssoAccount.name)) {
return API_CACHE.get(ssoAccount.name);
@@ -103,6 +117,7 @@ private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @Non
Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
final var nextcloudAPI = new NextcloudAPI(context.getApplicationContext(), ssoAccount,
new GsonBuilder()
+ .setStrictness(Strictness.LENIENT)
.excludeFieldsWithoutExposeAnnotation()
.registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.getTimeInMillis() / 1_000))
.registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer) (src, typeOfSrc, ctx) -> {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiResult.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiResult.kt
new file mode 100644
index 000000000..2f30ed4ac
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiResult.kt
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.persistence
+
+sealed class ApiResult {
+ data class Success(val data: T, val message: String? = null) : ApiResult()
+ data class Error(val message: String, val code: Int? = null) : ApiResult()
+}
+
+fun ApiResult.isSuccess(): Boolean = this is ApiResult.Success
+fun ApiResult.isError(): Boolean = this is ApiResult.Error
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
index af5389061..8b0d22f40 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java
@@ -52,6 +52,7 @@ public Result doWork() {
for (final var account : repo.getAccounts()) {
try {
final var ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName());
+
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
final var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag(), ApiProvider.getInstance());
repo.updateCapabilitiesETag(account.getId(), capabilities.getETag());
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
index 72507d588..1df1c22d1 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
@@ -17,8 +17,10 @@
import androidx.sqlite.db.SupportSQLiteDatabase;
import it.niedermann.owncloud.notes.persistence.dao.AccountDao;
+import it.niedermann.owncloud.notes.persistence.dao.CapabilitiesDao;
import it.niedermann.owncloud.notes.persistence.dao.CategoryOptionsDao;
import it.niedermann.owncloud.notes.persistence.dao.NoteDao;
+import it.niedermann.owncloud.notes.persistence.dao.ShareDao;
import it.niedermann.owncloud.notes.persistence.dao.WidgetNotesListDao;
import it.niedermann.owncloud.notes.persistence.dao.WidgetSingleNoteDao;
import it.niedermann.owncloud.notes.persistence.entity.Account;
@@ -26,6 +28,7 @@
import it.niedermann.owncloud.notes.persistence.entity.Converters;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+import it.niedermann.owncloud.notes.persistence.entity.ShareEntity;
import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
import it.niedermann.owncloud.notes.persistence.migration.Migration_10_11;
import it.niedermann.owncloud.notes.persistence.migration.Migration_11_12;
@@ -43,6 +46,7 @@
import it.niedermann.owncloud.notes.persistence.migration.Migration_23_24;
import it.niedermann.owncloud.notes.persistence.migration.Migration_24_25;
import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
@Database(
entities = {
@@ -50,8 +54,10 @@
Note.class,
CategoryOptions.class,
SingleNoteWidgetData.class,
- NotesListWidgetData.class
- }, version = 25
+ NotesListWidgetData.class,
+ ShareEntity.class,
+ Capabilities.class
+ }, version = 26
)
@TypeConverters({Converters.class})
public abstract class NotesDatabase extends RoomDatabase {
@@ -115,4 +121,8 @@ public void onCreate(@NonNull SupportSQLiteDatabase db) {
public abstract WidgetSingleNoteDao getWidgetSingleNoteDao();
public abstract WidgetNotesListDao getWidgetNotesListDao();
+
+ public abstract ShareDao getShareDao();
+
+ public abstract CapabilitiesDao getCapabilitiesDao();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
index 0ad24f3d1..7e00aec15 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
@@ -15,6 +15,7 @@
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets;
import static it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget.updateSingleNoteWidgets;
+
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
@@ -25,8 +26,11 @@
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.os.Handler;
+import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
+
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.MainThread;
@@ -37,11 +41,13 @@
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.preference.PreferenceManager;
+
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
@@ -51,6 +57,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+
import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
import it.niedermann.owncloud.notes.BuildConfig;
import it.niedermann.owncloud.notes.R;
@@ -60,6 +67,7 @@
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
+import it.niedermann.owncloud.notes.persistence.entity.ShareEntity;
import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.model.Capabilities;
@@ -99,6 +107,7 @@ public class NotesRepository {
private boolean syncOnlyOnWifi;
private final MutableLiveData syncStatus = new MutableLiveData<>(false);
private final MutableLiveData> syncErrors = new MutableLiveData<>();
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final Observer super ConnectionLiveData.ConnectionType> syncObserver = (Observer) connectionType -> {
observeNetworkStatus(connectionType);
@@ -157,7 +166,7 @@ private NotesRepository(@NonNull final Context context, @NonNull final NotesData
this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
this.connectionLiveData = new ConnectionLiveData(context);
- connectionLiveData.observeForever(syncObserver);
+ mainHandler.post(() -> connectionLiveData.observeForever(syncObserver));
final var prefs = PreferenceManager.getDefaultSharedPreferences(this.context);
prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
@@ -217,13 +226,15 @@ private void handleNetworkStatus(boolean isWifiActive) {
@Override
protected void finalize() throws Throwable {
- connectionLiveData.removeObserver(syncObserver);
+ mainHandler.post(() -> connectionLiveData.removeObserver(syncObserver));
super.finalize();
}
// Accounts
@AnyThread
public LiveData addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback callback) {
+ db.getCapabilitiesDao().insert(capabilities);
+
final var account = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, displayName, capabilities)));
if (account == null) {
callback.onError(new Exception("Could not read created account."));
@@ -284,6 +295,10 @@ public void deleteAccount(@NonNull Account account) {
db.getAccountDao().deleteAccount(account);
}
+ public Capabilities getCapabilities() {
+ return db.getCapabilitiesDao().getCapabilities();
+ }
+
public Account getAccountByName(String accountName) {
return db.getAccountDao().getAccountByName(accountName);
}
@@ -473,7 +488,7 @@ public NotesListWidgetData getNoteListWidgetData(int appWidgetId) {
@NonNull
@MainThread
public LiveData addNoteAndSync(Account account, Note note) {
- final var entity = new Note(0, null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), note.getETag(), DBStatus.LOCAL_EDITED, account.getId(), generateNoteExcerpt(note.getContent(), note.getTitle()), 0);
+ final var entity = new Note(0, null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), note.getETag(), DBStatus.LOCAL_EDITED, account.getId(), generateNoteExcerpt(note.getContent(), note.getTitle()), 0, note.isShared(), note.getReadonly());
final var ret = new MutableLiveData();
executor.submit(() -> ret.postValue(addNote(account.getId(), entity)));
return map(ret, newNote -> {
@@ -501,7 +516,7 @@ public Note addNote(long accountId, @NonNull Note note) {
@MainThread
public LiveData moveNoteToAnotherAccount(Account account, @NonNull Note note) {
- final var fullNote = new Note(null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), null);
+ final var fullNote = new Note(null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), null, note.isShared(), note.getReadonly());
fullNote.setStatus(DBStatus.LOCAL_EDITED);
deleteNoteAndSync(account, note.getId());
return addNoteAndSync(account, fullNote);
@@ -563,7 +578,7 @@ public Note updateNoteAndSync(@NonNull Account localAccount, @NonNull Note oldNo
// https://github.com/nextcloud/notes-android/issues/1198
@Nullable final Long remoteId = db.getNoteDao().getRemoteId(oldNote.getId());
if (newContent == null) {
- newNote = new Note(oldNote.getId(), remoteId, oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), remoteId, oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY(), oldNote.isShared(), oldNote.getReadonly());
} else {
final String title;
if (newTitle != null) {
@@ -577,7 +592,7 @@ public Note updateNoteAndSync(@NonNull Account localAccount, @NonNull Note oldNo
title = oldNote.getTitle();
}
}
- newNote = new Note(oldNote.getId(), remoteId, Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY());
+ newNote = new Note(oldNote.getId(), remoteId, Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY(), oldNote.isShared(), oldNote.getReadonly());
}
int rows = db.getNoteDao().updateNote(newNote);
// if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly.
@@ -959,4 +974,16 @@ public Call putServerSettings(@NonNull SingleSignOnAccount ssoAcc
public void updateDisplayName(long id, @Nullable String displayName) {
db.getAccountDao().updateDisplayName(id, displayName);
}
+
+ public void addShareEntities(List entities) {
+ db.getShareDao().addShareEntities(entities);
+ }
+
+ public List getShareEntities(String path) {
+ return db.getShareDao().getShareEntities(path);
+ }
+
+ public void updateNote(Note note) {
+ db.getNoteDao().updateNote(note);
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CapabilitiesDao.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CapabilitiesDao.kt
new file mode 100644
index 000000000..ed8b3d1c8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/CapabilitiesDao.kt
@@ -0,0 +1,16 @@
+package it.niedermann.owncloud.notes.persistence.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import it.niedermann.owncloud.notes.shared.model.Capabilities
+
+@Dao
+interface CapabilitiesDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insert(capabilities: Capabilities)
+
+ @Query("SELECT * FROM capabilities WHERE id = 1")
+ fun getCapabilities(): Capabilities
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
index 07b7e4f4a..6c1b48634 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
@@ -38,14 +38,14 @@ public interface NoteDao {
String getNoteById = "SELECT * FROM NOTE WHERE id = :id";
String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId";
String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1";
- String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
- String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
- String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC";
- String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC";
- String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC";
- String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
- String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC";
- String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC";
+ String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
+ String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
+ String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC";
+ String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC";
+ String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC";
+ String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
+ String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC";
+ String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC";
@Query(getNoteById)
LiveData getNoteById$(long id);
@@ -141,7 +141,7 @@ public interface NoteDao {
* Gets a list of {@link Note} objects with filled {@link Note#id} and {@link Note#remoteId},
* where {@link Note#remoteId} is not null
*/
- @Query("SELECT id, remoteId, 0 as accountId, '' as title, 0 as favorite, '' as excerpt, 0 as modified, '' as eTag, 0 as status, '' as category, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND remoteId IS NOT NULL")
+ @Query("SELECT id, remoteId, 0 as accountId, '' as title, 0 as favorite, 0 as isShared, 0 as readonly, '' as excerpt, 0 as modified, '' as eTag, 0 as status, '' as category, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND remoteId IS NOT NULL")
List getRemoteIdAndId(long accountId);
/**
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt
new file mode 100644
index 000000000..15ab98f80
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt
@@ -0,0 +1,16 @@
+package it.niedermann.owncloud.notes.persistence.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import it.niedermann.owncloud.notes.persistence.entity.ShareEntity
+
+@Dao
+interface ShareDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun addShareEntities(entities: List)
+
+ @Query("SELECT * FROM share_table WHERE path = :path")
+ fun getShareEntities(path: String): List
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
index a8fc3ba56..6b3c46a5b 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java
@@ -7,6 +7,7 @@
package it.niedermann.owncloud.notes.persistence.entity;
import android.graphics.Color;
+import android.net.Uri;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -93,6 +94,10 @@ public String getUrl() {
return url;
}
+ public String getAvatarUrl() {
+ return url + "/index.php/avatar/" + Uri.encode(userName) + "/64";
+ }
+
public void setUrl(@NonNull String url) {
this.url = url;
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
index 886eb8786..dabd3bd51 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java
@@ -38,6 +38,8 @@
@Index(name = "IDX_NOTE_ACCOUNTID", value = "accountId"),
@Index(name = "IDX_NOTE_CATEGORY", value = "category"),
@Index(name = "IDX_NOTE_FAVORITE", value = "favorite"),
+ @Index(name = "IDX_NOTE_IS_SHARED", value = "isShared"),
+ @Index(name = "IDX_READONLY", value = "readonly"),
@Index(name = "IDX_NOTE_MODIFIED", value = "modified"),
@Index(name = "IDX_NOTE_REMOTEID", value = "remoteId"),
@Index(name = "IDX_NOTE_STATUS", value = "status")
@@ -81,6 +83,14 @@ public class Note implements Serializable, Item {
@ColumnInfo(defaultValue = "0")
private boolean favorite = false;
+ @Expose
+ @ColumnInfo(defaultValue = "0")
+ private boolean isShared = false;
+
+ @Expose
+ @ColumnInfo(defaultValue = "0")
+ private boolean readonly = false;
+
@Expose
@Nullable
@SerializedName("etag")
@@ -99,6 +109,19 @@ public Note() {
@Ignore
public Note(@Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String eTag) {
+ this(remoteId,
+ modified,
+ title,
+ content,
+ category,
+ favorite,
+ eTag,
+ false,
+ false);
+ }
+
+ @Ignore
+ public Note(@Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String eTag, boolean isShared, boolean readonly) {
this.remoteId = remoteId;
this.title = title;
this.modified = modified;
@@ -106,11 +129,22 @@ public Note(@Nullable Long remoteId, @Nullable Calendar modified, @NonNull Strin
this.favorite = favorite;
this.category = category;
this.eTag = eTag;
+ this.isShared = isShared;
}
@Ignore
public Note(long id, @Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String etag, @NonNull DBStatus status, long accountId, @NonNull String excerpt, int scrollY) {
- this(remoteId, modified, title, content, category, favorite, etag);
+ this(remoteId, modified, title, content, category, favorite, etag, false, false);
+ this.id = id;
+ this.status = status;
+ this.accountId = accountId;
+ this.excerpt = excerpt;
+ this.scrollY = scrollY;
+ }
+
+ @Ignore
+ public Note(long id, @Nullable Long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, @NonNull String category, boolean favorite, @Nullable String etag, @NonNull DBStatus status, long accountId, @NonNull String excerpt, int scrollY, boolean isShared, boolean readonly) {
+ this(remoteId, modified, title, content, category, favorite, etag, isShared, readonly);
this.id = id;
this.status = status;
this.accountId = accountId;
@@ -126,6 +160,14 @@ public void setId(long id) {
this.id = id;
}
+ public boolean isShared() {
+ return isShared;
+ }
+
+ public void setIsShared(boolean value) {
+ this.isShared = value;
+ }
+
@NonNull
public String getCategory() {
return category;
@@ -188,6 +230,14 @@ public void setContent(@NonNull String content) {
this.content = content;
}
+ public boolean getReadonly() {
+ return readonly;
+ }
+
+ public void setReadonly(boolean value) {
+ readonly = value;
+ }
+
public boolean getFavorite() {
return favorite;
}
@@ -230,6 +280,7 @@ public boolean equals(Object o) {
if (id != note.id) return false;
if (accountId != note.accountId) return false;
if (favorite != note.favorite) return false;
+ if (isShared != note.isShared) return false;
if (scrollY != note.scrollY) return false;
if (!Objects.equals(remoteId, note.remoteId))
return false;
@@ -254,6 +305,7 @@ public int hashCode() {
result = 31 * result + (modified != null ? modified.hashCode() : 0);
result = 31 * result + content.hashCode();
result = 31 * result + (favorite ? 1 : 0);
+ result = 31 * result + (isShared ? 1 : 0);
result = 31 * result + (eTag != null ? eTag.hashCode() : 0);
result = 31 * result + excerpt.hashCode();
result = 31 * result + scrollY;
@@ -273,9 +325,10 @@ public String toString() {
", modified=" + modified +
", content='" + content + '\'' +
", favorite=" + favorite +
+ ", isShared=" + isShared +
", eTag='" + eTag + '\'' +
", excerpt='" + excerpt + '\'' +
", scrollY=" + scrollY +
'}';
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt
new file mode 100644
index 000000000..943a5e6aa
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt
@@ -0,0 +1,22 @@
+package it.niedermann.owncloud.notes.persistence.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "share_table")
+data class ShareEntity(
+ @PrimaryKey(autoGenerate = true)
+ @ColumnInfo(name = "id")
+ val id: Int? = null,
+ val note: String? = null,
+ val path: String? = null,
+ val file_target: String? = null,
+ val share_with: String? = null,
+ val share_with_displayname: String? = null,
+ val uid_file_owner: String? = null,
+ val displayname_file_owner: String? = null,
+ val uid_owner: String? = null,
+ val displayname_owner: String? = null,
+ val url: String? = null
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
index be211bd9e..b9b5e959c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java
@@ -40,13 +40,49 @@ public class CapabilitiesDeserializer implements JsonDeserializer
private static final String CAPABILITIES_FILES = "files";
private static final String CAPABILITIES_FILES_DIRECT_EDITING = "directEditing";
private static final String CAPABILITIES_FILES_DIRECT_EDITING_SUPPORTS_FILE_ID = "supportsFileId";
+ private static final String CAPABILITIES_FILES_SHARING = "files_sharing";
+ private static final String VERSION = "version";
@Override
public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
final var response = new Capabilities();
final var data = json.getAsJsonObject();
+ if (data.has(VERSION)) {
+ final var version = data.getAsJsonObject(VERSION);
+ final var nextcloudMajorVersion = version.get("major");
+ response.setNextcloudMajorVersion(String.valueOf(nextcloudMajorVersion));
+
+ final var nextcloudMinorVersion = version.get("minor");
+ response.setNextcloudMinorVersion(String.valueOf(nextcloudMinorVersion));
+
+
+ final var nextcloudMicroVersion = version.get("micro");
+ response.setNextcloudMicroVersion(String.valueOf(nextcloudMicroVersion));
+ }
+
if (data.has(CAPABILITIES)) {
final var capabilities = data.getAsJsonObject(CAPABILITIES);
+
+ if (capabilities.has(CAPABILITIES_FILES_SHARING)) {
+ final var filesSharing = capabilities.getAsJsonObject(CAPABILITIES_FILES_SHARING);
+ final var federation = filesSharing.getAsJsonObject("federation");
+ final var outgoing = federation.get("outgoing");
+
+ response.setFederationShare(outgoing.getAsBoolean());
+
+ final var publicObject = filesSharing.getAsJsonObject("public");
+ final var password = publicObject.getAsJsonObject("password");
+ final var enforced = password.getAsJsonPrimitive("enforced");
+ final var askForOptionalPassword = password.getAsJsonPrimitive("askForOptionalPassword");
+ final var isReSharingAllowed = filesSharing.getAsJsonPrimitive("resharing");
+ final var defaultPermission = filesSharing.getAsJsonPrimitive("default_permissions");
+
+ response.setDefaultPermission(defaultPermission.getAsInt());
+ response.setPublicPasswordEnforced(enforced.getAsBoolean());
+ response.setAskForOptionalPassword(askForOptionalPassword.getAsBoolean());
+ response.setReSharingAllowed(isReSharingAllowed.getAsBoolean());
+ }
+
if (capabilities.has(CAPABILITIES_NOTES)) {
final var notes = capabilities.getAsJsonObject(CAPABILITIES_NOTES);
if (notes.has(CAPABILITIES_NOTES_API_VERSION)) {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java
index fa12f0f0f..060cae353 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI_0_2.java
@@ -24,6 +24,7 @@
import retrofit2.http.Query;
/**
+ *
* @link Notes API v0.2
*/
public interface NotesAPI_0_2 {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt
new file mode 100644
index 000000000..53e3c0449
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt
@@ -0,0 +1,66 @@
+package it.niedermann.owncloud.notes.persistence.sync
+
+import com.google.gson.internal.LinkedTreeMap
+import it.niedermann.owncloud.notes.share.model.CreateShareRequest
+import it.niedermann.owncloud.notes.share.model.CreateShareResponse
+import it.niedermann.owncloud.notes.share.model.SharePasswordRequest
+import it.niedermann.owncloud.notes.share.model.UpdateSharePermissionRequest
+import it.niedermann.owncloud.notes.share.model.UpdateShareRequest
+import it.niedermann.owncloud.notes.shared.model.OcsResponse
+import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.DELETE
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.PUT
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface ShareAPI {
+ @GET("sharees")
+ fun getSharees(
+ @Query("format") format: String = "json",
+ @Query("itemType") itemType: String = "file",
+ @Query("search") search: String,
+ @Query("page") page: String,
+ @Query("perPage") perPage: String,
+ @Query("lookup") lookup: String = "false",
+ ): LinkedTreeMap?
+
+ @GET("shares/{remoteId}?format=json")
+ fun getShares(
+ @Path("remoteId") remoteId: Long,
+ @Query("include_tags") includeTags: Boolean = true,
+ ): Call>>
+
+ @GET("shares/?format=json")
+ fun getSharesForSpecificNote(
+ @Query("path") path: String,
+ @Query("reshares") reshares: Boolean = true,
+ @Query("subfiles") subfiles: Boolean = true
+ ): LinkedTreeMap?
+
+ @DELETE("shares/{shareId}?format=json")
+ fun removeShare(@Path("shareId") shareId: Long): Call
+
+ @POST("shares?format=json")
+ fun addShare(@Body request: CreateShareRequest): Call>
+
+ @POST("shares/{shareId}/send-email?format=json")
+ fun sendEmail(@Path("shareId") shareId: Long, @Body password: SharePasswordRequest?): Call
+
+ @PUT("shares/{shareId}?format=json")
+ fun updateShare(@Path("shareId") shareId: Long, @Body request: UpdateShareRequest): Call>
+
+ @PUT("shares/{shareId}?format=json")
+ fun updateSharePermission(
+ @Path("shareId") shareId: Long,
+ @Body request: UpdateSharePermissionRequest
+ ): Call>
+
+ @GET("shares/?format=json")
+ fun getShareFromNote(
+ @Query("path") path: String,
+ @Query("shared_with_me") sharedWithMe: Boolean = true
+ ): Call>>
+}
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
new file mode 100644
index 000000000..a27dd5b24
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java
@@ -0,0 +1,856 @@
+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 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.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.util.ArrayList;
+import java.util.Collections;
+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.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;
+
+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 void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ executorService = Executors.newSingleThreadScheduledExecutor();
+ binding = ActivityNoteShareBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ 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() {
+ 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.getSharesForNotesAndSaveShareEntities();
+
+ runOnUiThread(() -> {
+ 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) {
+ throw new RuntimeException(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_NULL);
+
+ 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.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, "", "false", "", 0, "");
+ 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 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 tempShares = new ArrayList<>();
+
+ // to show share with users/groups info
+ if (note != null) {
+ // get shares from local DB
+ populateSharesList(tempShares);
+ }
+
+ runOnUiThread(() -> {
+ shares.clear();
+ shares.addAll(tempShares);
+
+ adapter.removeAll();
+ adapter.addShares(shares);
+ addPublicShares(adapter);
+ setShareWithYou();
+
+ isInitializingShares = false;
+ });
+ } catch (Exception e) {
+ Log_OC.d(TAG, "Exception while updateShareeListAdapter: " + e);
+ }
+ });
+ }
+
+ private void populateSharesList(List targetList) {
+ // Get shares from local DB
+ final var shareEntities = repository.getShareEntitiesForSpecificNote(note);
+ for (var entity : shareEntities) {
+ if (entity.getId() != null) {
+ addSharesToList(entity.getId(), targetList);
+ }
+ }
+
+ // Get shares from remote
+ final var remoteShares = repository.getShareFromNote(note);
+ if (remoteShares != null) {
+ for (var entity : remoteShares) {
+ addSharesToList(entity.getId(), targetList);
+ }
+ }
+ }
+
+ private void addSharesToList(long id, List targetList) {
+ final var result = repository.getShares(id);
+ if (result != null) {
+ for (OCShare ocShare : result) {
+ if (!targetList.contains(ocShare)) {
+ targetList.add(ocShare);
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+
+ @Override
+ public void sendNewEmail(OCShare share) {
+ modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE);
+ }
+
+ @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) {
+ 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());
+
+ 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) {
+ executorService.submit(() -> {
+ try {
+ final var updatedShares = repository.getShares(share.getId());
+
+ runOnUiThread(() -> {
+ if (updatedShares != null && 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.androidx.themeToolbarSearchView(binding.searchView);
+ util.platform.themeHorizontalProgressBar(binding.progressBar);
+ }
+
+ @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(() -> {
+ {
+ final var requestBody = repository.getUpdateShareRequest(
+ false,
+ share,
+ "",
+ password,
+ false,
+ -1,
+ share.getPermissions()
+ );
+ 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/NoteShareDetailActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt
new file mode 100644
index 000000000..bfa39fd43
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt
@@ -0,0 +1,611 @@
+package it.niedermann.owncloud.notes.share
+
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+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.SharePermissionsBuilder
+import com.owncloud.android.lib.resources.shares.ShareType
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandedActivity
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import it.niedermann.owncloud.notes.databinding.ActivityNoteShareDetailBinding
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.persistence.isSuccess
+import it.niedermann.owncloud.notes.share.dialog.ExpirationDatePickerDialogFragment
+import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper
+import it.niedermann.owncloud.notes.share.model.SharePasswordRequest
+import it.niedermann.owncloud.notes.share.repository.ShareRepository
+import it.niedermann.owncloud.notes.shared.util.DisplayUtils
+import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil
+import it.niedermann.owncloud.notes.shared.util.extensions.getParcelableArgument
+import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.text.SimpleDateFormat
+import java.util.Date
+
+/**
+ * Activity class to show share permission options, set expiration date, change label, set password, send note
+ *
+ * This activity handles following:
+ * 1. This will be shown while creating new internal and external share. So that user can set every share
+ * configuration at one time.
+ * 2. This will handle both Advanced Permissions and Send New Email functionality for existing shares to modify them.
+ */
+@Suppress("TooManyFunctions")
+class NoteShareDetailActivity : BrandedActivity(),
+ ExpirationDatePickerDialogFragment.OnExpiryDateListener {
+
+ companion object {
+ const val TAG = "NoteShareDetailActivity"
+ const val ARG_NOTE = "arg_sharing_note"
+ const val ARG_SHAREE_NAME = "arg_sharee_name"
+ const val ARG_SHARE_TYPE = "arg_share_type"
+ const val ARG_OCSHARE = "arg_ocshare"
+ const val ARG_SCREEN_TYPE = "arg_screen_type"
+ const val ARG_RESHARE_SHOWN = "arg_reshare_shown"
+ const val ARG_EXP_DATE_SHOWN = "arg_exp_date_shown"
+ private const val ARG_SECURE_SHARE = "secure_share"
+
+ // types of screens to be displayed
+ const val SCREEN_TYPE_PERMISSION = 1 // permissions screen
+ const val SCREEN_TYPE_NOTE = 2 // note screen
+ }
+
+ private lateinit var binding: ActivityNoteShareDetailBinding
+ private var note: Note? = null // note to be share
+ private var shareeName: String? = null
+ private lateinit var shareType: ShareType
+ private var shareProcessStep = SCREEN_TYPE_PERMISSION // default screen type
+ private var permission = OCShare.NO_PERMISSION // no permission
+ private var chosenExpDateInMills: Long = -1 // for no expiry date
+
+ private var share: OCShare? = null
+ private var isReShareShown: Boolean = true // show or hide reShare option
+ private var isExpDateShown: Boolean = true // show or hide expiry date option
+ private var isSecureShare: Boolean = false
+
+ private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
+ private lateinit var repository: ShareRepository
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = ActivityNoteShareDetailBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ val arguments = intent.extras
+
+ arguments?.let {
+ note = it.getSerializableArgument(ARG_NOTE, Note::class.java)
+ shareeName = it.getString(ARG_SHAREE_NAME)
+ share = it.getParcelableArgument(ARG_OCSHARE, OCShare::class.java)
+
+ if (it.containsKey(ARG_SHARE_TYPE)) {
+ shareType = ShareType.fromValue(it.getInt(ARG_SHARE_TYPE))
+ } else if (share != null) {
+ shareType = share!!.shareType!!
+ }
+
+ shareProcessStep = it.getInt(ARG_SCREEN_TYPE, SCREEN_TYPE_PERMISSION)
+ isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true)
+ isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
+ isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false)
+ }
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ val ssoAcc =
+ SingleAccountHelper.getCurrentSingleSignOnAccount(this@NoteShareDetailActivity)
+ repository = ShareRepository(this@NoteShareDetailActivity, ssoAcc)
+ permission = repository.getCapabilities().defaultPermission
+
+ withContext(Dispatchers.Main) {
+ if (shareProcessStep == SCREEN_TYPE_PERMISSION) {
+ showShareProcessFirst()
+ } else {
+ showShareProcessSecond()
+ }
+ implementClickEvents()
+ }
+ }
+ }
+
+
+ override fun applyBrand(color: Int) {
+ val util = BrandingUtil.of(color, this)
+
+ binding.run {
+ util.platform.run {
+ themeRadioButton(shareProcessPermissionReadOnly)
+ themeRadioButton(shareProcessPermissionUploadEditing)
+ themeRadioButton(shareProcessPermissionFileDrop)
+
+ colorTextView(shareProcessEditShareLink)
+ colorTextView(shareProcessAdvancePermissionTitle)
+
+ themeCheckbox(shareProcessAllowResharingCheckbox)
+ }
+
+ util.androidx.run {
+ colorSwitchCompat(shareProcessSetPasswordSwitch)
+ colorSwitchCompat(shareProcessSetExpDateSwitch)
+ colorSwitchCompat(shareProcessHideDownloadCheckbox)
+ colorSwitchCompat(shareProcessChangeNameSwitch)
+ }
+
+ util.material.run {
+ colorTextInputLayout(shareProcessEnterPasswordContainer)
+ colorTextInputLayout(shareProcessChangeNameContainer)
+ colorTextInputLayout(noteContainer)
+
+ colorMaterialButtonPrimaryFilled(shareProcessBtnNext)
+ colorMaterialButtonPrimaryOutlined(shareProcessBtnCancel)
+ }
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ // Force recreation of dialog activity when screen rotates
+ // This is needed because the calendar layout should be different in portrait and landscape,
+ // but as FDA persists through config changes, the dialog is not recreated automatically
+ val datePicker = expirationDatePickerFragment
+ if (datePicker?.dialog?.isShowing == true) {
+ val currentSelectionMillis = datePicker.currentSelectionMillis
+ datePicker.dismiss()
+ showExpirationDateDialog(currentSelectionMillis)
+ }
+ }
+
+ private fun showShareProcessFirst() {
+ binding.shareProcessGroupOne.visibility = View.VISIBLE
+ binding.shareProcessEditShareLink.visibility = View.VISIBLE
+ binding.shareProcessGroupTwo.visibility = View.GONE
+
+ if (share != null) {
+ setupModificationUI()
+ } else {
+ setupUpdateUI()
+ }
+
+ if (isSecureShare) {
+ binding.shareProcessAdvancePermissionTitle.visibility = View.GONE
+ }
+
+ // show or hide expiry date
+ if (isExpDateShown && !isSecureShare) {
+ binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
+ } else {
+ binding.shareProcessSetExpDateSwitch.visibility = View.GONE
+ }
+ shareProcessStep = SCREEN_TYPE_PERMISSION
+ }
+
+ private fun setupModificationUI() {
+ if (share?.isFolder == true) updateViewForFolder() else updateViewForFile()
+
+ // read only / allow upload and editing / file drop
+ if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
+ binding.shareProcessPermissionUploadEditing.isChecked = true
+ } else if (SharingMenuHelper.isFileDrop(share) && share?.isFolder == true) {
+ binding.shareProcessPermissionFileDrop.isChecked = true
+ } else if (SharingMenuHelper.isReadOnly(share)) {
+ binding.shareProcessPermissionReadOnly.isChecked = true
+ }
+
+ shareType = share?.shareType ?: ShareType.NO_SHARED
+
+ // show different text for link share and other shares
+ // because we have link to share in Public Link
+ binding.shareProcessBtnNext.text = getString(
+ if (shareType == ShareType.PUBLIC_LINK) {
+ R.string.note_share_detail_activity_share_copy_link
+ } else {
+ R.string.note_share_detail_activity_common_confirm
+ }
+ )
+
+ updateViewForShareType()
+ binding.shareProcessSetPasswordSwitch.isChecked = share?.isPasswordProtected == true
+ showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked)
+ updateExpirationDateView()
+ showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked)
+ }
+
+ private fun setupUpdateUI() {
+ binding.shareProcessBtnNext.text =
+ getString(R.string.note_share_detail_activity_common_next)
+ note.let {
+ updateViewForFile()
+ updateViewForShareType()
+ }
+ showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked)
+ showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked)
+ }
+
+ private fun updateViewForShareType() {
+ when (shareType) {
+ ShareType.EMAIL -> {
+ updateViewForExternalShare()
+ }
+
+ ShareType.PUBLIC_LINK -> {
+ updateViewForLinkShare()
+ }
+
+ else -> {
+ updateViewForInternalShare()
+ }
+ }
+ }
+
+ private fun updateViewForExternalShare() {
+ binding.shareProcessChangeNameSwitch.visibility = View.GONE
+ binding.shareProcessChangeNameContainer.visibility = View.GONE
+ updateViewForExternalAndLinkShare()
+ }
+
+ private fun updateViewForLinkShare() {
+ updateViewForExternalAndLinkShare()
+ binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE
+ if (share != null) {
+ binding.shareProcessChangeName.setText(share?.label)
+ binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
+ }
+ showChangeNameInput(binding.shareProcessChangeNameSwitch.isChecked)
+ }
+
+ private fun updateViewForInternalShare() {
+ binding.run {
+ shareProcessChangeNameSwitch.visibility = View.GONE
+ shareProcessChangeNameContainer.visibility = View.GONE
+ shareProcessHideDownloadCheckbox.visibility = View.GONE
+ if (isSecureShare) {
+ shareProcessAllowResharingCheckbox.visibility = View.GONE
+ } else {
+ shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
+ }
+ shareProcessSetPasswordSwitch.visibility = View.GONE
+
+ if (share != null) {
+ if (!isReShareShown) {
+ shareProcessAllowResharingCheckbox.visibility = View.GONE
+ }
+ shareProcessAllowResharingCheckbox.isChecked =
+ SharingMenuHelper.canReshare(share)
+ }
+ }
+ }
+
+ /**
+ * update views where share type external or link share
+ */
+ private fun updateViewForExternalAndLinkShare() {
+ binding.run {
+ shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
+ shareProcessAllowResharingCheckbox.visibility = View.GONE
+ shareProcessSetPasswordSwitch.visibility = View.VISIBLE
+
+ if (share != null) {
+ if (SharingMenuHelper.isFileDrop(share)) {
+ shareProcessHideDownloadCheckbox.visibility = View.GONE
+ } else {
+ shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
+ shareProcessHideDownloadCheckbox.isChecked =
+ share?.isHideFileDownload == true
+ }
+ }
+ }
+ }
+
+ /**
+ * update expiration date view while modifying the share
+ */
+ private fun updateExpirationDateView() {
+ share?.let { share ->
+ if (share.expirationDate > 0) {
+ chosenExpDateInMills = share.expirationDate
+ binding.shareProcessSetExpDateSwitch.isChecked = true
+ binding.shareProcessSelectExpDate.text = getString(
+ R.string.share_expiration_date_format,
+ SimpleDateFormat.getDateInstance().format(Date(share.expirationDate))
+ )
+ }
+ }
+ }
+
+ private fun updateViewForFile() {
+ binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_editing)
+ binding.shareProcessPermissionFileDrop.visibility = View.GONE
+ }
+
+ private fun updateViewForFolder() {
+ binding.run {
+ shareProcessPermissionUploadEditing.text =
+ getString(R.string.link_share_allow_upload_and_editing)
+ shareProcessPermissionFileDrop.visibility = View.VISIBLE
+ if (isSecureShare) {
+ shareProcessPermissionFileDrop.visibility = View.GONE
+ shareProcessAllowResharingCheckbox.visibility = View.GONE
+ shareProcessSetExpDateSwitch.visibility = View.GONE
+ }
+ }
+ }
+
+ /**
+ * update views for screen type Note
+ */
+ private fun showShareProcessSecond() {
+ binding.run {
+ shareProcessGroupOne.visibility = View.GONE
+ shareProcessEditShareLink.visibility = View.GONE
+ shareProcessGroupTwo.visibility = View.VISIBLE
+ if (share != null) {
+ shareProcessBtnNext.text =
+ getString(R.string.note_share_detail_activity_set_note)
+ noteText.setText(share?.note)
+ } else {
+ shareProcessBtnNext.text =
+ getString(R.string.note_share_detail_activity_send_share)
+ noteText.setText(R.string.empty)
+ }
+ shareProcessStep = SCREEN_TYPE_NOTE
+ shareProcessBtnNext.performClick()
+ }
+ }
+
+ private fun implementClickEvents() {
+ binding.run {
+ shareProcessBtnCancel.setOnClickListener {
+ onCancelClick()
+ }
+ shareProcessBtnNext.setOnClickListener {
+ if (shareProcessStep == SCREEN_TYPE_PERMISSION) {
+ validateShareProcessFirst()
+ } else {
+ createOrUpdateShare()
+ }
+ }
+ shareProcessSetPasswordSwitch.setOnCheckedChangeListener { _, isChecked ->
+ showPasswordInput(isChecked)
+ }
+ shareProcessSetExpDateSwitch.setOnCheckedChangeListener { _, isChecked ->
+ showExpirationDateInput(isChecked)
+ }
+ shareProcessChangeNameSwitch.setOnCheckedChangeListener { _, isChecked ->
+ showChangeNameInput(isChecked)
+ }
+ shareProcessSelectExpDate.setOnClickListener {
+ showExpirationDateDialog()
+ }
+ }
+ }
+
+ private fun showExpirationDateDialog(chosenDateInMillis: Long = chosenExpDateInMills) {
+ val dialog = ExpirationDatePickerDialogFragment.newInstance(chosenDateInMillis)
+ dialog.setOnExpiryDateListener(this)
+ expirationDatePickerFragment = dialog
+ dialog.show(supportFragmentManager, ExpirationDatePickerDialogFragment.DATE_PICKER_DIALOG)
+ }
+
+ private fun showChangeNameInput(isChecked: Boolean) {
+ binding.shareProcessChangeNameContainer.visibility =
+ if (isChecked) View.VISIBLE else View.GONE
+ if (!isChecked) {
+ binding.shareProcessChangeName.setText(R.string.empty)
+ }
+ }
+
+ private fun onCancelClick() {
+ // if modifying the existing share then on back press remove the current activity
+ if (share != null) {
+ finish()
+ }
+
+ // else we have to check if user is in step 2(note screen) then show step 1 (permission screen)
+ // and if user is in step 1 (permission screen) then remove the activity
+ else {
+ if (shareProcessStep == SCREEN_TYPE_NOTE) {
+ showShareProcessFirst()
+ } else {
+ finish()
+ }
+ }
+ }
+
+ private fun showExpirationDateInput(isChecked: Boolean) {
+ binding.shareProcessSelectExpDate.visibility = if (isChecked) View.VISIBLE else View.GONE
+ binding.shareProcessExpDateDivider.visibility = if (isChecked) View.VISIBLE else View.GONE
+
+ // reset the expiration date if switch is unchecked
+ if (!isChecked) {
+ chosenExpDateInMills = -1
+ binding.shareProcessSelectExpDate.text = getString(R.string.empty)
+ }
+ }
+
+ private fun showPasswordInput(isChecked: Boolean) {
+ binding.shareProcessEnterPasswordContainer.visibility =
+ if (isChecked) View.VISIBLE else View.GONE
+
+ // reset the password if switch is unchecked
+ if (!isChecked) {
+ binding.shareProcessEnterPassword.setText(R.string.empty)
+ }
+ }
+
+
+ private fun getReSharePermission(): Int {
+ return SharePermissionsBuilder().apply {
+ setSharePermission(true)
+ }.build()
+ }
+
+ /**
+ * method to validate the step 1 screen information
+ */
+ @Suppress("ReturnCount")
+ private fun validateShareProcessFirst() {
+ permission = getSelectedPermission()
+ if (permission == OCShare.NO_PERMISSION) {
+ DisplayUtils.showSnackMessage(
+ binding.root,
+ R.string.note_share_detail_activity_no_share_permission_selected
+ )
+ return
+ }
+
+ if (binding.shareProcessSetPasswordSwitch.isChecked &&
+ binding.shareProcessEnterPassword.text?.trim().isNullOrEmpty()
+ ) {
+ DisplayUtils.showSnackMessage(
+ binding.root,
+ R.string.note_share_detail_activity_share_link_empty_password
+ )
+ return
+ }
+
+ if (binding.shareProcessSetExpDateSwitch.isChecked &&
+ binding.shareProcessSelectExpDate.text?.trim().isNullOrEmpty()
+ ) {
+ showExpirationDateDialog()
+ return
+ }
+
+ if (binding.shareProcessChangeNameSwitch.isChecked &&
+ binding.shareProcessChangeName.text?.trim().isNullOrEmpty()
+ ) {
+ DisplayUtils.showSnackMessage(
+ binding.root,
+ R.string.note_share_detail_activity_label_empty
+ )
+ return
+ }
+
+ // if modifying existing share information then execute the process
+ if (share != null) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val noteText = binding.noteText.text.toString().trim()
+ val password = binding.shareProcessEnterPassword.text.toString().trim()
+
+ updateShare(noteText, password, false)
+ }
+ } else {
+ // else show step 2 (note screen)
+ showShareProcessSecond()
+ }
+ }
+
+ /**
+ * get the permissions on the basis of selection
+ */
+ private fun getSelectedPermission() = when {
+ binding.shareProcessAllowResharingCheckbox.isChecked -> getReSharePermission()
+ binding.shareProcessPermissionReadOnly.isChecked -> OCShare.READ_PERMISSION_FLAG
+ binding.shareProcessPermissionUploadEditing.isChecked -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE
+ binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG
+ else -> permission
+ }
+
+ /**
+ * method to validate step 2 (note screen) information
+ */
+ private fun createOrUpdateShare() {
+ val noteText = binding.noteText.text.toString().trim()
+ val password = binding.shareProcessEnterPassword.text.toString().trim()
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ if (share != null && share?.note != noteText) {
+ updateShare(noteText, password, true)
+ } else {
+ createShare(noteText, password)
+ }
+ }
+ }
+
+ private suspend fun updateShare(noteText: String, password: String, sendEmail: Boolean) {
+ val downloadPermission = !binding.shareProcessHideDownloadCheckbox.isChecked
+ val requestBody = repository.getUpdateShareRequest(
+ downloadPermission,
+ share,
+ noteText,
+ password,
+ sendEmail,
+ chosenExpDateInMills,
+ permission
+ )
+
+ val updateShareResult = repository.updateShare(share!!.id, requestBody)
+
+ if (updateShareResult.isSuccess() && sendEmail) {
+ val sendEmailResult = repository.sendEmail(share!!.id, SharePasswordRequest(password))
+ handleResult(sendEmailResult)
+ } else {
+ handleResult(updateShareResult.isSuccess())
+ }
+
+ if (!sendEmail) {
+ withContext(Dispatchers.Main) {
+ if (!TextUtils.isEmpty(share?.shareLink)) {
+ ClipboardUtil.copyToClipboard(this@NoteShareDetailActivity, share?.shareLink)
+ }
+ }
+ }
+ }
+
+ private suspend fun createShare(noteText: String, password: String) {
+ if (note == null || shareeName == null) {
+ Log_OC.d(TAG, "validateShareProcessSecond cancelled")
+ return
+ }
+
+ val result = repository.addShare(
+ note!!,
+ shareType,
+ shareeName!!,
+ "false", // TODO: Check how to determine it
+ password,
+ permission,
+ noteText
+ )
+
+ if (result.isSuccess()) {
+ repository.getSharesForNotesAndSaveShareEntities()
+ }
+
+ handleResult(result.isSuccess())
+ }
+
+ private suspend fun handleResult(success: Boolean) {
+ withContext(Dispatchers.Main) {
+ if (success) {
+ val resultIntent = Intent()
+ setResult(RESULT_OK, resultIntent)
+ finish()
+ } else {
+ DisplayUtils.showSnackMessage(
+ this@NoteShareDetailActivity,
+ getString(R.string.note_share_detail_activity_create_share_error)
+ )
+ }
+ }
+ }
+
+ override fun onDateSet(year: Int, monthOfYear: Int, dayOfMonth: Int, chosenDateInMillis: Long) {
+ binding.shareProcessSelectExpDate.text = getString(
+ R.string.share_expiration_date_format,
+ SimpleDateFormat.getDateInstance().format(Date(chosenDateInMillis))
+ )
+ this.chosenExpDateInMills = chosenDateInMillis
+ }
+
+ override fun onDateUnSet() {
+ binding.shareProcessSetExpDateSwitch.isChecked = false
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt
new file mode 100644
index 000000000..84e335008
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt
@@ -0,0 +1,64 @@
+package it.niedermann.owncloud.notes.share.adapter
+
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import it.niedermann.owncloud.notes.databinding.ItemQuickSharePermissionsBinding
+import it.niedermann.owncloud.notes.share.model.QuickPermissionModel
+
+class QuickSharingPermissionsAdapter(
+ private val quickPermissionList: MutableList,
+ private val onPermissionChangeListener: QuickSharingPermissionViewHolder.OnPermissionChangeListener,
+) :
+ RecyclerView.Adapter() {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val binding = ItemQuickSharePermissionsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return QuickSharingPermissionViewHolder(binding, binding.root, onPermissionChangeListener)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder is QuickSharingPermissionViewHolder) {
+ holder.bindData(quickPermissionList[position])
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return quickPermissionList.size
+ }
+
+ class QuickSharingPermissionViewHolder(
+ val binding: ItemQuickSharePermissionsBinding,
+ itemView: View,
+ val onPermissionChangeListener: OnPermissionChangeListener,
+ ) :
+ RecyclerView
+ .ViewHolder(itemView) {
+
+ fun bindData(quickPermissionModel: QuickPermissionModel) {
+ binding.tvQuickShareName.text = quickPermissionModel.permissionName
+ if (quickPermissionModel.isSelected) {
+ // viewThemeUtils.platform.colorImageView(binding.tvQuickShareCheckIcon)
+ binding.tvQuickShareCheckIcon.visibility = View.VISIBLE
+ } else {
+ binding.tvQuickShareCheckIcon.visibility = View.INVISIBLE
+ }
+
+ itemView.setOnClickListener {
+ // if user select different options then only update the permission
+ if (!quickPermissionModel.isSelected) {
+ onPermissionChangeListener.onPermissionChanged(adapterPosition)
+ } else {
+ // dismiss sheet on selection of same permission
+ onPermissionChangeListener.onDismissSheet()
+ }
+ }
+ }
+
+ interface OnPermissionChangeListener {
+ fun onPermissionChanged(position: Int)
+ fun onDismissSheet()
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java
new file mode 100644
index 000000000..f234c4a11
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java
@@ -0,0 +1,231 @@
+package it.niedermann.owncloud.notes.share.adapter;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+import com.owncloud.android.lib.resources.shares.ShareType;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import it.niedermann.owncloud.notes.databinding.ItemAddPublicShareBinding;
+import it.niedermann.owncloud.notes.databinding.ItemInternalShareLinkBinding;
+import it.niedermann.owncloud.notes.databinding.ItemShareLinkShareBinding;
+import it.niedermann.owncloud.notes.databinding.ItemShareShareBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.share.adapter.holder.InternalShareViewHolder;
+import it.niedermann.owncloud.notes.share.adapter.holder.LinkShareViewHolder;
+import it.niedermann.owncloud.notes.share.adapter.holder.NewLinkShareViewHolder;
+import it.niedermann.owncloud.notes.share.adapter.holder.ShareViewHolder;
+import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener;
+
+/**
+ * Adapter to show a user/group/email/remote in Sharing list in file details view.
+ */
+public class ShareeListAdapter extends RecyclerView.Adapter {
+
+ private final Account account;
+ private final ShareeListAdapterListener listener;
+ private final Activity activity;
+ private List shares;
+
+ public ShareeListAdapter(Activity activity,
+ List shares,
+ ShareeListAdapterListener listener,
+ Account account) {
+ this.activity = activity;
+ this.shares = shares;
+ this.listener = listener;
+ this.account = account;
+
+ sortShares();
+ setHasStableIds(true);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (shares == null) {
+ return 0;
+ }
+
+ if (position < 0 || position >= shares.size()) {
+ return 0;
+ }
+
+ final var share = shares.get(position);
+ if (share == null) {
+ return 0;
+ }
+
+ final var shareType = share.getShareType();
+ if (shareType == null) {
+ return 0;
+ }
+
+ return shareType.getValue();
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (ShareType.fromValue(viewType)) {
+ case PUBLIC_LINK, EMAIL -> {
+ return new LinkShareViewHolder(
+ ItemShareLinkShareBinding.inflate(LayoutInflater.from(activity),
+ parent,
+ false),
+ activity);
+ }
+ case NEW_PUBLIC_LINK -> {
+ return new NewLinkShareViewHolder(
+ ItemAddPublicShareBinding.inflate(LayoutInflater.from(activity),
+ parent,
+ false)
+ );
+ }
+ case INTERNAL -> {
+ return new InternalShareViewHolder(
+ ItemInternalShareLinkBinding.inflate(LayoutInflater.from(activity), parent, false),
+ activity);
+ }
+ default -> {
+ return new ShareViewHolder(ItemShareShareBinding.inflate(LayoutInflater.from(activity),
+ parent,
+ false),
+ account,
+ activity);
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ if (shares == null || shares.size() <= position) {
+ return;
+ }
+
+ final OCShare share = shares.get(position);
+
+
+ if (holder instanceof LinkShareViewHolder publicShareViewHolder) {
+ publicShareViewHolder.bind(share, listener);
+ } else if (holder instanceof InternalShareViewHolder internalShareViewHolder) {
+ internalShareViewHolder.bind(share, listener);
+ } else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) {
+ newLinkShareViewHolder.bind(listener);
+ } else {
+ ShareViewHolder userViewHolder = (ShareViewHolder) holder;
+ userViewHolder.bind(share, listener);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (position < 0 || position >= shares.size()) {
+ return 0;
+ }
+
+ return shares.get(position).getId();
+ }
+
+ @Override
+ public int getItemCount() {
+ return shares.size();
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public void addShares(List sharesToAdd) {
+ Set uniqueShares = new LinkedHashSet<>(shares);
+
+ // Automatically removes duplicates
+ uniqueShares.addAll(sharesToAdd);
+
+ shares.clear();
+ shares.addAll(uniqueShares);
+
+ sortShares();
+ notifyDataSetChanged();
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public void remove(OCShare share) {
+ shares.remove(share);
+ notifyDataSetChanged();
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public void removeAll() {
+ shares.clear();
+ notifyDataSetChanged();
+ }
+
+ public void addShare(@NonNull OCShare newShare) {
+ shares.add(0, newShare);
+ notifyItemInserted(0);
+ }
+
+ public void updateShare(@NonNull OCShare updatedShare) {
+ int indexToUpdate = -1;
+ for (int i = 0; i < getItemCount(); i++) {
+ final var share = shares.get(i);
+ if (share != null && share.getId() == updatedShare.getId()) {
+ indexToUpdate = i;
+ break;
+ }
+ }
+
+ if (indexToUpdate != -1) {
+ shares.set(indexToUpdate, updatedShare);
+ notifyItemChanged(indexToUpdate);
+ }
+ }
+
+ /**
+ * sort all by creation time, then email/link shares on top
+ */
+ protected final void sortShares() {
+ List links = new ArrayList<>();
+ List users = new ArrayList<>();
+
+ for (OCShare share : shares) {
+ if (share.getShareType() != null) {
+ if (ShareType.PUBLIC_LINK == share.getShareType() || ShareType.EMAIL == share.getShareType()) {
+ links.add(share);
+ } else if (share.getShareType() != ShareType.INTERNAL) {
+ users.add(share);
+ }
+ }
+ }
+
+ links.sort((o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
+ users.sort((o1, o2) -> Long.compare(o2.getSharedDate(), o1.getSharedDate()));
+
+ shares = links;
+ shares.addAll(users);
+
+ final OCShare ocShare = new OCShare();
+ ocShare.setShareType(ShareType.INTERNAL);
+ shares.add(ocShare);
+ }
+
+ public List getShares() {
+ return shares;
+ }
+
+ public void removeNewPublicShare() {
+ for (OCShare share : shares) {
+ if (share.getShareType() == ShareType.NEW_PUBLIC_LINK) {
+ shares.remove(share);
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt
new file mode 100644
index 000000000..f057c7124
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt
@@ -0,0 +1,52 @@
+package it.niedermann.owncloud.notes.share.adapter
+
+import android.app.SearchManager
+import android.content.Context
+import android.database.Cursor
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.database.getIntOrNull
+import androidx.core.database.getStringOrNull
+import androidx.cursoradapter.widget.CursorAdapter
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.persistence.entity.Account
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader
+
+class SuggestionAdapter(context: Context, cursor: Cursor?, private val account: Account) : CursorAdapter(context, cursor, false) {
+ override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
+ val inflater = LayoutInflater.from(context)
+ return inflater.inflate(R.layout.item_suggestion_adapter, parent, false)
+ }
+
+ override fun bindView(view: View, context: Context, cursor: Cursor) {
+ val suggestion =
+ cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1))
+ view.findViewById(R.id.suggestion_text).text = suggestion
+
+
+ val icon = view.findViewById(R.id.suggestion_icon)
+ val iconColumn = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1)
+
+ if (iconColumn != -1) {
+ try {
+ val iconId = cursor.getIntOrNull(iconColumn)
+ if (iconId != null) {
+ icon.setImageDrawable(ContextCompat.getDrawable(context, iconId))
+ }
+ } catch (e: Exception) {
+ try {
+ val username = cursor.getStringOrNull(iconColumn)
+ if (username != null) {
+ AvatarLoader.load(context, icon, account, username)
+ }
+ } catch (e: Exception) {
+ icon.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_account_circle_grey_24dp))
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java
new file mode 100644
index 000000000..c322022b1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java
@@ -0,0 +1,55 @@
+package it.niedermann.owncloud.notes.share.adapter.holder;
+
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemInternalShareLinkBinding;
+import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener;
+
+public class InternalShareViewHolder extends RecyclerView.ViewHolder {
+ private ItemInternalShareLinkBinding binding;
+ private Context context;
+
+ public InternalShareViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ public InternalShareViewHolder(ItemInternalShareLinkBinding binding, Context context) {
+ this(binding.getRoot());
+ this.binding = binding;
+ this.context = context;
+ }
+
+ public void bind(OCShare share, ShareeListAdapterListener listener) {
+ binding.copyInternalLinkIcon
+ .getBackground()
+ .setColorFilter(ResourcesCompat.getColor(context.getResources(),
+ R.color.widget_foreground,
+ null),
+ PorterDuff.Mode.SRC_IN);
+ binding.copyInternalLinkIcon
+ .getDrawable()
+ .mutate()
+ .setColorFilter(ResourcesCompat.getColor(context.getResources(),
+ R.color.fg_contrast,
+ null),
+ PorterDuff.Mode.SRC_IN);
+
+ if (share.isFolder()) {
+ binding.shareInternalLinkText.setText(context.getString(R.string.share_internal_link_to_folder_text));
+ } else {
+ binding.shareInternalLinkText.setText(context.getString(R.string.share_internal_link_to_file_text));
+ }
+
+ binding.copyInternalContainer.setOnClickListener(l -> listener.copyInternalLink());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java
new file mode 100644
index 000000000..0f6bb3864
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java
@@ -0,0 +1,92 @@
+package it.niedermann.owncloud.notes.share.adapter.holder;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.text.TextUtils;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.res.ResourcesCompat;
+
+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.BrandedViewHolder;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.ItemShareLinkShareBinding;
+import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper;
+import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener;
+
+public class LinkShareViewHolder extends BrandedViewHolder {
+ private ItemShareLinkShareBinding binding;
+ private Context context;
+
+ public LinkShareViewHolder(@NonNull View itemView) {
+ super(itemView);
+ bindBranding();
+ }
+
+ public LinkShareViewHolder(ItemShareLinkShareBinding binding, Context context) {
+ this(binding.getRoot());
+ this.binding = binding;
+ this.context = context;
+ bindBranding();
+ }
+
+ public void bind(OCShare publicShare, ShareeListAdapterListener listener) {
+ if (publicShare.getShareType() != null && ShareType.EMAIL == publicShare.getShareType()) {
+ binding.name.setText(publicShare.getSharedWithDisplayName());
+ binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(),
+ R.drawable.ic_email,
+ null));
+ binding.copyLink.setVisibility(View.GONE);
+
+ binding.icon.getBackground().setColorFilter(context.getResources().getColor(R.color.nc_grey),
+ PorterDuff.Mode.SRC_IN);
+ binding.icon.getDrawable().mutate().setColorFilter(context.getResources().getColor(R.color.icon_on_nc_grey),
+ PorterDuff.Mode.SRC_IN);
+ } else {
+ if (!TextUtils.isEmpty(publicShare.getLabel())) {
+ String text = String.format(context.getString(R.string.share_link_with_label), publicShare.getLabel());
+ binding.name.setText(text);
+ } else {
+ if (SharingMenuHelper.isSecureFileDrop(publicShare)) {
+ binding.name.setText(context.getResources().getString(R.string.share_permission_secure_file_drop));
+ } else {
+ binding.name.setText(R.string.share_link);
+ }
+ }
+ }
+
+ binding.subline.setVisibility(View.GONE);
+
+ String permissionName = SharingMenuHelper.getPermissionName(context, publicShare);
+ setPermissionName(publicShare, permissionName);
+
+ binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare));
+ if (!SharingMenuHelper.isSecureFileDrop(publicShare)) {
+ binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare));
+ }
+
+ binding.copyLink.setOnClickListener(v -> listener.copyLink(publicShare));
+ }
+
+ private void setPermissionName(OCShare publicShare, String permissionName) {
+ if (!TextUtils.isEmpty(permissionName) && !SharingMenuHelper.isSecureFileDrop(publicShare)) {
+ binding.permissionName.setText(permissionName);
+ binding.permissionName.setVisibility(View.VISIBLE);
+ } else {
+ binding.permissionName.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void applyBrand(int color) {
+ final var util = BrandingUtil.of(color, context);
+ if (binding != null) {
+ util.androidx.colorPrimaryTextViewElement(binding.permissionName);
+ util.platform.colorImageViewBackgroundAndIcon(binding.icon);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/NewLinkShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/NewLinkShareViewHolder.java
new file mode 100644
index 000000000..f2e795a0b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/NewLinkShareViewHolder.java
@@ -0,0 +1,26 @@
+package it.niedermann.owncloud.notes.share.adapter.holder;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.databinding.ItemAddPublicShareBinding;
+import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener;
+
+public class NewLinkShareViewHolder extends RecyclerView.ViewHolder {
+ private ItemAddPublicShareBinding binding;
+
+ public NewLinkShareViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ public NewLinkShareViewHolder(ItemAddPublicShareBinding binding) {
+ this(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(ShareeListAdapterListener listener) {
+ binding.addNewPublicShareLink.setOnClickListener(v -> listener.createPublicShareLink());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java
new file mode 100644
index 000000000..52c3ff526
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java
@@ -0,0 +1,118 @@
+package it.niedermann.owncloud.notes.share.adapter.holder;
+
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemShareShareBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
+import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper;
+import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener;
+import it.niedermann.owncloud.notes.shared.util.FilesSpecificViewThemeUtils;
+
+public class ShareViewHolder extends RecyclerView.ViewHolder {
+ private ItemShareShareBinding binding;
+ private Account account;
+ private Context context;
+
+ public ShareViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ public ShareViewHolder(ItemShareShareBinding binding,
+ Account account,
+ Context context) {
+ this(binding.getRoot());
+ this.binding = binding;
+ this.account = account;
+ this.context = context;
+ }
+
+ public void bind(OCShare share, ShareeListAdapterListener listener) {
+ String accountName = account.getDisplayName();
+ String name = share.getSharedWithDisplayName();
+ binding.icon.setTag(null);
+ final var shareType = share.getShareType();
+ if (shareType == null) {
+ return;
+ }
+
+ final var viewThemeUtils = FilesSpecificViewThemeUtils.INSTANCE;
+
+ switch (shareType) {
+ case GROUP:
+ name = context.getString(R.string.share_group_clarification, name);
+ viewThemeUtils.createAvatar(share.getShareType(), binding.icon, context);
+ break;
+ case ROOM:
+ name = context.getString(R.string.share_room_clarification, name);
+ viewThemeUtils.createAvatar(share.getShareType(), binding.icon, context);
+ break;
+ case CIRCLE:
+ viewThemeUtils.createAvatar(share.getShareType(), binding.icon, context);
+ break;
+ case FEDERATED:
+ name = context.getString(R.string.share_remote_clarification, name);
+ setImage(binding.icon, share.getSharedWithDisplayName(), R.drawable.ic_account_circle_grey_24dp);
+ break;
+ case USER:
+ binding.icon.setTag(share.getShareWith());
+
+ if (share.getSharedWithDisplayName() != null) {
+ AvatarLoader.INSTANCE.load(context, binding.icon, account, share.getSharedWithDisplayName());
+ }
+
+ // binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith()));
+ default:
+ setImage(binding.icon, name, R.drawable.ic_account_circle_grey_24dp);
+ break;
+ }
+
+ binding.name.setText(name);
+
+ if (accountName == null) {
+ binding.overflowMenu.setVisibility(View.GONE);
+ return;
+ }
+
+ if (accountName.equalsIgnoreCase(share.getShareWith()) || accountName.equalsIgnoreCase(share.getUserId())) {
+ binding.overflowMenu.setVisibility(View.VISIBLE);
+
+ String permissionName = SharingMenuHelper.getPermissionName(context, share);
+ setPermissionName(permissionName);
+
+ // bind listener to edit privileges
+ binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(share));
+ binding.shareNameLayout.setOnClickListener(v -> listener.showPermissionsDialog(share));
+ } else {
+ binding.overflowMenu.setVisibility(View.GONE);
+ }
+ }
+
+ private void setPermissionName(String permissionName) {
+ if (!TextUtils.isEmpty(permissionName)) {
+ binding.permissionName.setText(permissionName);
+ binding.permissionName.setVisibility(View.VISIBLE);
+ } else {
+ binding.permissionName.setVisibility(View.GONE);
+ }
+ }
+
+ private void setImage(ImageView avatar, String name, @DrawableRes int fallback) {
+ try {
+ AvatarLoader.INSTANCE.load(context, avatar, account, name);
+ } catch (StringIndexOutOfBoundsException e) {
+ avatar.setImageResource(fallback);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ExpirationDatePickerDialogFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ExpirationDatePickerDialogFragment.kt
new file mode 100644
index 000000000..54d74dfd1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ExpirationDatePickerDialogFragment.kt
@@ -0,0 +1,167 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2023 Alper Ozturk
+ * SPDX-FileCopyrightText: 2022 Álvaro Brey
+ * SPDX-FileCopyrightText: 2021 2018 TSI-mc
+ * SPDX-FileCopyrightText: 2018 Tobias Kaminsky
+ * SPDX-FileCopyrightText: 2018 Andy Scherzinger
+ * SPDX-FileCopyrightText: 2015 David A. Velasco
+ * SPDX-FileCopyrightText: 2015 ownCloud Inc.
+ * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
+ */
+package it.niedermann.owncloud.notes.share.dialog
+
+import android.app.DatePickerDialog
+import android.app.DatePickerDialog.OnDateSetListener
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.widget.DatePicker
+import com.google.android.material.button.MaterialButton
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandedDialogFragment
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import java.util.Calendar
+
+/**
+ * Dialog requesting a date after today.
+ */
+class ExpirationDatePickerDialogFragment : BrandedDialogFragment(), OnDateSetListener {
+
+
+ private var onExpiryDateListener: OnExpiryDateListener? = null
+
+ fun setOnExpiryDateListener(onExpiryDateListener: OnExpiryDateListener?) {
+ this.onExpiryDateListener = onExpiryDateListener
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return A new dialog to let the user choose an expiration date that will be bound to a share link.
+ */
+ @Suppress("DEPRECATION", "MagicNumber")
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ // Chosen date received as an argument must be later than tomorrow ; default to tomorrow in other case
+ val chosenDate = Calendar.getInstance()
+ val tomorrowInMillis = chosenDate.timeInMillis + DateUtils.DAY_IN_MILLIS
+ val chosenDateInMillis = requireArguments().getLong(ARG_CHOSEN_DATE_IN_MILLIS)
+ chosenDate.timeInMillis = chosenDateInMillis.coerceAtLeast(tomorrowInMillis)
+
+ // Create a new instance of DatePickerDialog
+ val dialog = DatePickerDialog(
+ requireActivity(),
+ R.style.FallbackDatePickerDialogTheme,
+ this,
+ chosenDate[Calendar.YEAR],
+ chosenDate[Calendar.MONTH],
+ chosenDate[Calendar.DAY_OF_MONTH]
+ )
+
+ // show unset button only when date is already selected
+ if (chosenDateInMillis > 0) {
+ dialog.setButton(
+ Dialog.BUTTON_NEGATIVE,
+ getText(R.string.share_via_link_unset_password)
+ ) { _: DialogInterface?, _: Int ->
+ onExpiryDateListener?.onDateUnSet()
+ }
+ }
+
+ // Prevent days in the past may be chosen
+ val picker = dialog.datePicker
+ picker.minDate = tomorrowInMillis - 1000
+
+ // Enforce spinners view; ignored by MD-based theme in Android >=5, but calendar is REALLY buggy
+ // in Android < 5, so let's be sure it never appears (in tablets both spinners and calendar are
+ // shown by default)
+ @Suppress("DEPRECATION")
+ picker.calendarViewShown = false
+ return dialog
+ }
+
+ override fun applyBrand(color: Int) {
+ val util = BrandingUtil.of(color, requireContext())
+ val currentDialog = dialog
+
+ if (currentDialog != null) {
+ val dialog = currentDialog as DatePickerDialog
+
+ val positiveButton = dialog.getButton(DatePickerDialog.BUTTON_POSITIVE) as MaterialButton?
+ if (positiveButton != null) {
+ util?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+ }
+ val negativeButton = dialog.getButton(DatePickerDialog.BUTTON_NEGATIVE) as MaterialButton?
+ if (negativeButton != null) {
+ util?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+ }
+ val neutralButton = dialog.getButton(DatePickerDialog.BUTTON_NEUTRAL) as MaterialButton?
+ if (neutralButton != null) {
+ util?.material?.colorMaterialButtonPrimaryBorderless(neutralButton)
+ }
+ }
+ }
+
+ val currentSelectionMillis: Long
+ get() {
+ val dialog = dialog
+ if (dialog != null) {
+ val datePickerDialog = dialog as DatePickerDialog
+ val picker = datePickerDialog.datePicker
+ return yearMonthDayToMillis(picker.year, picker.month, picker.dayOfMonth)
+ }
+ return 0
+ }
+
+ /**
+ * Called when the user chooses an expiration date.
+ *
+ * @param view View instance where the date was chosen
+ * @param year Year of the date chosen.
+ * @param monthOfYear Month of the date chosen [0, 11]
+ * @param dayOfMonth Day of the date chosen
+ */
+ override fun onDateSet(view: DatePicker, year: Int, monthOfYear: Int, dayOfMonth: Int) {
+ val chosenDateInMillis = yearMonthDayToMillis(year, monthOfYear, dayOfMonth)
+ if (onExpiryDateListener != null) {
+ onExpiryDateListener?.onDateSet(year, monthOfYear, dayOfMonth, chosenDateInMillis)
+ }
+ }
+
+ private fun yearMonthDayToMillis(year: Int, monthOfYear: Int, dayOfMonth: Int): Long {
+ val date = Calendar.getInstance()
+ date[Calendar.YEAR] = year
+ date[Calendar.MONTH] = monthOfYear
+ date[Calendar.DAY_OF_MONTH] = dayOfMonth
+ return date.timeInMillis
+ }
+
+ interface OnExpiryDateListener {
+ fun onDateSet(year: Int, monthOfYear: Int, dayOfMonth: Int, chosenDateInMillis: Long)
+ fun onDateUnSet()
+ }
+
+ companion object {
+ /** Tag for FragmentsManager */
+ const val DATE_PICKER_DIALOG = "DATE_PICKER_DIALOG"
+
+ /** Parameter constant for date chosen initially */
+ private const val ARG_CHOSEN_DATE_IN_MILLIS = "CHOSEN_DATE_IN_MILLIS"
+
+ /**
+ * Factory method to create new instances
+ *
+ * @param chosenDateInMillis Date chosen when the dialog appears
+ * @return New dialog instance
+ */
+ fun newInstance(chosenDateInMillis: Long): ExpirationDatePickerDialogFragment {
+ val arguments = Bundle()
+ arguments.putLong(ARG_CHOSEN_DATE_IN_MILLIS, chosenDateInMillis)
+ val dialog = ExpirationDatePickerDialogFragment()
+ dialog.arguments = arguments
+ return dialog
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java
new file mode 100644
index 000000000..82340bd4a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java
@@ -0,0 +1,111 @@
+package it.niedermann.owncloud.notes.share.dialog;
+
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.nextcloud.android.common.ui.theme.utils.ColorRole;
+import com.owncloud.android.lib.resources.shares.OCShare;
+import com.owncloud.android.lib.resources.shares.ShareType;
+
+import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialog;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.ItemNoteShareActionBinding;
+import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper;
+import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction;
+
+public class NoteShareActivityShareItemActionBottomSheetDialog extends BrandedBottomSheetDialog {
+ private ItemNoteShareActionBinding binding;
+ private final NoteShareItemAction actions;
+ private final OCShare ocShare;
+ public NoteShareActivityShareItemActionBottomSheetDialog(Activity activity,
+ NoteShareItemAction actions,
+ OCShare ocShare) {
+ super(activity);
+ this.actions = actions;
+ this.ocShare = ocShare;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ItemNoteShareActionBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ if (getWindow() != null) {
+ getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ updateUI();
+
+ setupClickListener();
+
+ setOnShowListener(d ->
+ BottomSheetBehavior.from((View) binding.getRoot().getParent())
+ .setPeekHeight(binding.getRoot().getMeasuredHeight())
+ );
+ }
+
+ private void updateUI() {
+ if (ocShare.getShareType() != null && ocShare.getShareType() == ShareType.PUBLIC_LINK) {
+ binding.menuShareAddAnotherLink.setVisibility(View.VISIBLE);
+ binding.menuShareSendLink.setVisibility(View.VISIBLE);
+ } else {
+ binding.menuShareAddAnotherLink.setVisibility(View.GONE);
+ binding.menuShareSendLink.setVisibility(View.GONE);
+ }
+
+ if (SharingMenuHelper.isSecureFileDrop(ocShare)) {
+ binding.menuShareAdvancedPermissions.setVisibility(View.GONE);
+ binding.menuShareAddAnotherLink.setVisibility(View.GONE);
+ }
+ }
+
+ private void setupClickListener() {
+ binding.menuShareAdvancedPermissions.setOnClickListener(v -> {
+ actions.advancedPermissions(ocShare);
+ dismiss();
+ });
+
+ binding.menuShareSendNewEmail.setOnClickListener(v -> {
+ actions.sendNewEmail(ocShare);
+ dismiss();
+ });
+
+ binding.menuShareUnshare.setOnClickListener(v -> {
+ actions.unShare(ocShare);
+ dismiss();
+ });
+
+ binding.menuShareSendLink.setOnClickListener(v -> {
+ actions.sendLink(ocShare);
+ dismiss();
+ });
+
+ binding.menuShareAddAnotherLink.setOnClickListener(v -> {
+ actions.addAnotherLink(ocShare);
+ dismiss();
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ binding = null;
+ }
+
+ @Override
+ public void applyBrand(int color) {
+ final var util = BrandingUtil.of(color, getContext());
+ util.platform.themeDialog(binding.getRoot());
+
+ util.platform.colorImageView(binding.menuIconAddAnotherLink, ColorRole.PRIMARY);
+ util.platform.colorImageView(binding.menuIconAdvancedPermissions, ColorRole.PRIMARY);
+ util.platform.colorImageView(binding.menuIconSendLink, ColorRole.PRIMARY);
+ util.platform.colorImageView(binding.menuIconUnshare, ColorRole.PRIMARY);
+ util.platform.colorImageView(binding.menuIconSendNewEmail, ColorRole.PRIMARY);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt
new file mode 100644
index 000000000..eb80e83e7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ProfileBottomSheetDialog.kt
@@ -0,0 +1,144 @@
+package it.niedermann.owncloud.notes.share.dialog
+
+
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.FragmentActivity
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.nextcloud.android.lib.resources.profile.Action
+import com.nextcloud.android.lib.resources.profile.HoverCard
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.databinding.ProfileBottomSheetActionBinding
+import it.niedermann.owncloud.notes.databinding.ProfileBottomSheetFragmentBinding
+import it.niedermann.owncloud.notes.persistence.entity.Account
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader
+import it.niedermann.owncloud.notes.shared.util.DisplayUtils
+
+/**
+ * Show actions of an user
+ */
+class ProfileBottomSheetDialog(
+ private val fileActivity: FragmentActivity,
+ private val account: Account,
+ private val hoverCard: HoverCard,
+) : BottomSheetDialog(fileActivity) {
+ private var _binding: ProfileBottomSheetFragmentBinding? = null
+
+ // This property is only valid between onCreateView and onDestroyView.
+ val binding get() = _binding!!
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ _binding = ProfileBottomSheetFragmentBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ if (window != null) {
+ window!!.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+
+ // viewThemeUtils.platform.themeDialog(binding.root)
+
+ binding.icon.tag = hoverCard.userId
+
+
+ /*
+ // TODO: How to get owner display name from note?
+
+ binding.sharedWithYouUsername.setText(
+ String.format(getString(R.string.note_share_fragment_shared_with_you), file.getOwnerDisplayName()));
+ */
+ AvatarLoader.load(fileActivity, binding.icon, account)
+
+ binding.displayName.text = hoverCard.displayName
+
+ for (action in hoverCard.actions) {
+ val actionBinding = ProfileBottomSheetActionBinding.inflate(
+ layoutInflater
+ )
+ val creatorView: View = actionBinding.root
+
+ if (action.appId == "email") {
+ action.hyperlink = action.title
+ action.title = context.resources.getString(R.string.write_email)
+ }
+
+ actionBinding.name.text = action.title
+
+ val icon = when (action.appId) {
+ "profile" -> R.drawable.ic_account_circle_grey_24dp
+ "email" -> R.drawable.ic_email
+ "spreed" -> R.drawable.ic_talk
+ else -> R.drawable.ic_edit
+ }
+ actionBinding.icon.setImageDrawable(
+ ResourcesCompat.getDrawable(
+ context.resources,
+ icon,
+ null
+ )
+ )
+ // viewThemeUtils.platform.tintPrimaryDrawable(context, actionBinding.icon.drawable)
+
+ creatorView.setOnClickListener { v: View? ->
+ send(hoverCard.userId, action)
+ dismiss()
+ }
+ binding.creators.addView(creatorView)
+ }
+
+ setOnShowListener { d: DialogInterface? ->
+ BottomSheetBehavior.from(binding.root.parent as View)
+ .setPeekHeight(binding.root.measuredHeight)
+ }
+ }
+
+ private fun send(userId: String, action: Action) {
+ when (action.appId) {
+ "profile" -> openWebsite(action.hyperlink)
+ "core" -> sendEmail(action.hyperlink)
+ "spreed" -> openTalk(userId, action.hyperlink)
+ }
+ }
+
+ private fun openWebsite(url: String) {
+ DisplayUtils.startLinkIntent(fileActivity, url)
+ }
+
+ private fun sendEmail(email: String) {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:")
+ putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
+ }
+
+ DisplayUtils.startIntentIfAppAvailable(intent, fileActivity, R.string.no_email_app_available)
+ }
+
+ private fun openTalk(userId: String, hyperlink: String) {
+ // TODO:
+ /*
+ try {
+ val sharingIntent = Intent(Intent.ACTION_VIEW)
+ sharingIntent.setClassName(
+ "com.nextcloud.talk2",
+ "com.nextcloud.talk.activities.MainActivity"
+ )
+ sharingIntent.putExtra("server", user.server.uri)
+ sharingIntent.putExtra("userId", userId)
+ fileActivity.startActivity(sharingIntent)
+ } catch (e: ActivityNotFoundException) {
+ openWebsite(hyperlink)
+ }
+ */
+
+ }
+
+ override fun onStop() {
+ super.onStop()
+ _binding = null
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java
new file mode 100644
index 000000000..f250e0523
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java
@@ -0,0 +1,146 @@
+package it.niedermann.owncloud.notes.share.dialog;
+
+
+import static com.owncloud.android.lib.resources.shares.OCShare.CREATE_PERMISSION_FLAG;
+import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FILE;
+import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER;
+import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.QuickSharingPermissionsBottomSheetFragmentBinding;
+import it.niedermann.owncloud.notes.share.adapter.QuickSharingPermissionsAdapter;
+import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper;
+import it.niedermann.owncloud.notes.share.model.QuickPermissionModel;
+
+/**
+ * File Details Quick Sharing permissions options {@link android.app.Dialog} styled as a bottom sheet for main actions.
+ */
+public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog {
+ private QuickSharingPermissionsBottomSheetFragmentBinding binding;
+ private final QuickPermissionSharingBottomSheetActions actions;
+ private final Activity activity;
+ private final OCShare ocShare;
+
+ public QuickSharingPermissionsBottomSheetDialog(Activity activity,
+ QuickPermissionSharingBottomSheetActions actions,
+ OCShare ocShare) {
+ super(activity);
+ this.actions = actions;
+ this.ocShare = ocShare;
+ this.activity = activity;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = QuickSharingPermissionsBottomSheetFragmentBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ if (getWindow() != null) {
+ getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ // viewThemeUtils.platform.themeDialog(binding.getRoot());
+
+ setUpRecyclerView();
+ setOnShowListener(d ->
+ BottomSheetBehavior.from((View) binding.getRoot().getParent())
+ .setPeekHeight(binding.getRoot().getMeasuredHeight())
+ );
+ }
+
+ private void setUpRecyclerView() {
+ List quickPermissionModelList = getQuickPermissionList();
+ QuickSharingPermissionsAdapter adapter = new QuickSharingPermissionsAdapter(
+ quickPermissionModelList,
+ new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() {
+ @Override
+ public void onPermissionChanged(int position) {
+ handlePermissionChanged(quickPermissionModelList, position);
+ }
+
+ @Override
+ public void onDismissSheet() {
+ dismiss();
+ }
+ }
+ );
+ LinearLayoutManager linearLayoutManager = new LinearLayoutManager(activity);
+ binding.rvQuickSharePermissions.setLayoutManager(linearLayoutManager);
+ binding.rvQuickSharePermissions.setAdapter(adapter);
+ }
+
+ private void handlePermissionChanged(List quickPermissionModelList, int position) {
+ if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string.link_share_allow_upload_and_editing))
+ || quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string.link_share_editing))) {
+ if (ocShare.isFolder()) {
+ actions.onQuickPermissionChanged(ocShare,
+ MAXIMUM_PERMISSIONS_FOR_FOLDER);
+ } else {
+ actions.onQuickPermissionChanged(ocShare,
+ MAXIMUM_PERMISSIONS_FOR_FILE);
+ }
+ } else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string
+ .link_share_view_only))) {
+ actions.onQuickPermissionChanged(ocShare,
+ READ_PERMISSION_FLAG);
+
+ } else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string
+ .link_share_file_drop))) {
+ actions.onQuickPermissionChanged(ocShare,
+ CREATE_PERMISSION_FLAG);
+ }
+ dismiss();
+ }
+
+ /**
+ * prepare the list of permissions needs to be displayed on recyclerview
+ * @return
+ */
+ private List getQuickPermissionList() {
+
+ String[] permissionArray;
+ if (ocShare.isFolder()) {
+ permissionArray =
+ activity.getResources().getStringArray(R.array.quick_sharing_permission_bottom_sheet_dialog_folder_share_values);
+ } else {
+ permissionArray =
+ activity.getResources().getStringArray(R.array.quick_sharing_permission_bottom_sheet_dialog_note_share_values);
+ }
+ //get the checked item position
+ int checkedItem = SharingMenuHelper.getPermissionCheckedItem(activity, ocShare, permissionArray);
+
+
+ final List quickPermissionModelList = new ArrayList<>(permissionArray.length);
+ for (int i = 0; i < permissionArray.length; i++) {
+ QuickPermissionModel quickPermissionModel = new QuickPermissionModel(permissionArray[i], checkedItem == i);
+ quickPermissionModelList.add(quickPermissionModel);
+ }
+ return quickPermissionModelList;
+ }
+
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ binding = null;
+ }
+
+ public interface QuickPermissionSharingBottomSheetActions {
+ void onQuickPermissionChanged(OCShare share, int permission);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkDialog.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkDialog.kt
new file mode 100644
index 000000000..08faf11e9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/ShareLinkDialog.kt
@@ -0,0 +1,141 @@
+package it.niedermann.owncloud.notes.share.dialog
+
+
+import android.app.Dialog
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.owncloud.android.lib.common.utils.Log_OC
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.shared.util.clipboard.CopyToClipboardActivity
+import it.niedermann.owncloud.notes.shared.util.extensions.getParcelableArgument
+import java.util.Collections
+
+/**
+ * Dialog showing a list activities able to resolve a given Intent,
+ * filtering out the activities matching give package names.
+ */
+class ShareLinkToDialog : DialogFragment() {
+ private var mAdapter: ActivityAdapter? = null
+ private var mIntent: Intent? = null
+
+ init {
+ Log_OC.d(TAG, "constructor")
+ }
+
+ @Suppress("SpreadOperator")
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ mIntent = arguments.getParcelableArgument(ARG_INTENT, Intent::class.java) ?: throw NullPointerException()
+ val packagesToExclude = arguments?.getStringArray(ARG_PACKAGES_TO_EXCLUDE)
+ val packagesToExcludeList = listOf(*packagesToExclude ?: arrayOfNulls(0))
+ val pm = activity?.packageManager ?: throw NullPointerException()
+
+ val activities = pm.queryIntentActivities(mIntent!!, PackageManager.MATCH_DEFAULT_ONLY)
+ val it = activities.iterator()
+ var resolveInfo: ResolveInfo
+ while (it.hasNext()) {
+ resolveInfo = it.next()
+ if (packagesToExcludeList.contains(resolveInfo.activityInfo.packageName.lowercase())) {
+ it.remove()
+ }
+ }
+
+ val sendAction = mIntent?.getBooleanExtra(Intent.ACTION_SEND, false)
+
+ if (sendAction == false) {
+ // add activity for copy to clipboard
+ val copyToClipboardIntent = Intent(requireActivity(), CopyToClipboardActivity::class.java)
+ val copyToClipboard = pm.queryIntentActivities(copyToClipboardIntent, 0)
+ if (copyToClipboard.isNotEmpty()) {
+ activities.add(copyToClipboard[0])
+ }
+ }
+
+ Collections.sort(activities, ResolveInfo.DisplayNameComparator(pm))
+ mAdapter = ActivityAdapter(requireActivity(), pm, activities)
+
+ return createSelector(sendAction ?: false)
+ }
+
+ private fun createSelector(sendAction: Boolean): AlertDialog {
+ val titleId = if (sendAction) {
+ R.string.activity_chooser_send_file_title
+ } else {
+ R.string.activity_chooser_title
+ }
+
+ return MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(titleId)
+ .setAdapter(mAdapter) { _, which ->
+ // Add the information of the chosen activity to the intent to send
+ val chosen = mAdapter?.getItem(which)
+ val actInfo = chosen?.activityInfo ?: return@setAdapter
+ val name = ComponentName(
+ actInfo.applicationInfo.packageName,
+ actInfo.name
+ )
+ mIntent?.setComponent(name)
+ activity?.startActivity(mIntent)
+ }
+ .create()
+ }
+
+ internal inner class ActivityAdapter(
+ context: Context,
+ private val mPackageManager: PackageManager,
+ apps: List
+ ) : ArrayAdapter(context, R.layout.activity_row, apps) {
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view = convertView ?: newView(parent)
+ bindView(position, view)
+ return view
+ }
+
+ private fun newView(parent: ViewGroup): View {
+ return (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
+ .inflate(R.layout.activity_row, parent, false)
+ }
+
+ private fun bindView(position: Int, row: View) {
+ row.findViewById(R.id.title).run {
+ text = getItem(position)?.loadLabel(mPackageManager)
+ }
+
+ row.findViewById(R.id.icon).run {
+ setImageDrawable(getItem(position)?.loadIcon(mPackageManager))
+ }
+ }
+ }
+
+ companion object {
+ private val TAG: String = ShareLinkToDialog::class.java.simpleName
+ private val ARG_INTENT = ShareLinkToDialog::class.java.simpleName +
+ ".ARG_INTENT"
+ private val ARG_PACKAGES_TO_EXCLUDE = ShareLinkToDialog::class.java.simpleName +
+ ".ARG_PACKAGES_TO_EXCLUDE"
+
+ @JvmStatic
+ fun newInstance(intent: Intent?, vararg packagesToExclude: String?): ShareLinkToDialog {
+ val bundle = Bundle().apply {
+ putParcelable(ARG_INTENT, intent)
+ putStringArray(ARG_PACKAGES_TO_EXCLUDE, packagesToExclude)
+ }
+
+ return ShareLinkToDialog().apply {
+ arguments = bundle
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/SharePasswordDialogFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/SharePasswordDialogFragment.kt
new file mode 100644
index 000000000..4a1646698
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/SharePasswordDialogFragment.kt
@@ -0,0 +1,247 @@
+package it.niedermann.owncloud.notes.share.dialog
+
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.TextUtils
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import com.google.android.material.button.MaterialButton
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import com.owncloud.android.lib.resources.shares.OCShare
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandedDialogFragment
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import it.niedermann.owncloud.notes.databinding.PasswordDialogBinding
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.shared.util.KeyboardUtils
+import it.niedermann.owncloud.notes.shared.util.extensions.getParcelableArgument
+import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument
+
+/**
+ * Dialog to input the password for sharing a file/folder.
+ *
+ *
+ * Triggers the share when the password is introduced.
+ */
+class SharePasswordDialogFragment : BrandedDialogFragment() {
+
+ private var keyboardUtils: KeyboardUtils? = null
+
+ private var binding: PasswordDialogBinding? = null
+ private var note: Note? = null
+ private var share: OCShare? = null
+ private var createShare = false
+ private var askForPassword = false
+ private var builder: MaterialAlertDialogBuilder? = null
+ private var listener: SharePasswordDialogListener? = null
+
+ interface SharePasswordDialogListener {
+ fun shareFileViaPublicShare(note: Note?, password: String?)
+ fun setPasswordToShare(share: OCShare, password: String?)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val alertDialog = dialog as AlertDialog?
+ if (alertDialog == null) {
+ return
+ }
+
+ val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton?
+ positiveButton?.setOnClickListener {
+ val sharePassword = binding?.sharePassword?.text
+
+ if (sharePassword != null) {
+ val password = sharePassword.toString()
+ if (!askForPassword && TextUtils.isEmpty(password)) {
+ BrandedSnackbar.make(
+ binding!!.root,
+ getString(R.string.note_share_detail_activity_share_link_empty_password),
+ Snackbar.LENGTH_LONG
+ )
+ .show()
+ return@setOnClickListener
+ }
+ if (share == null) {
+ listener?.shareFileViaPublicShare(note, password)
+ } else {
+ listener?.setPasswordToShare(share!!, password)
+ }
+ }
+
+ alertDialog.dismiss()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ keyboardUtils?.showKeyboardForEditText(binding!!.sharePassword)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ note = requireArguments().getSerializableArgument(ARG_FILE, Note::class.java)
+ share = requireArguments().getParcelableArgument(ARG_SHARE, OCShare::class.java)
+
+ createShare = requireArguments().getBoolean(ARG_CREATE_SHARE, false)
+ askForPassword = requireArguments().getBoolean(ARG_ASK_FOR_PASSWORD, false)
+
+ // Inflate the layout for the dialog
+ val inflater = requireActivity().layoutInflater
+ binding = PasswordDialogBinding.inflate(inflater, null, false)
+
+ // Setup layout
+ binding?.sharePassword?.setText(R.string.empty)
+
+ val neutralButtonTextId: Int
+ val title: Int
+ if (askForPassword) {
+ title = R.string.share_password_dialog_fragment_share_link_optional_password_title
+ neutralButtonTextId = R.string.share_password_dialog_fragment_skip
+ } else {
+ title = R.string.share_link_password_title
+ neutralButtonTextId = R.string.note_share_detail_activity_cancel
+ }
+
+ // Build the dialog
+ builder = MaterialAlertDialogBuilder(requireContext())
+ builder!!.setView(binding!!.root)
+ .setPositiveButton(R.string.common_ok, null)
+ .setNegativeButton(R.string.common_delete) { _: DialogInterface?, _: Int -> callSetPassword() }
+ .setNeutralButton(neutralButtonTextId) { _: DialogInterface?, _: Int ->
+ if (askForPassword) {
+ callSetPassword()
+ }
+ }
+ .setTitle(title)
+
+ return builder!!.create()
+ }
+
+ private fun callSetPassword() {
+ val password = binding?.sharePassword?.text.toString().trim()
+ if (share == null) {
+ listener?.shareFileViaPublicShare(note, password)
+ } else {
+ listener?.setPasswordToShare(share!!, password)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ binding = null
+ }
+
+ override fun applyBrand(color: Int) {
+ val util = BrandingUtil.of(color, requireContext())
+ builder?.let {
+ util?.dialog?.colorMaterialAlertDialogBackground(requireContext(), it)
+ }
+
+ val alertDialog = dialog as AlertDialog?
+ val positiveButton = alertDialog?.getButton(AlertDialog.BUTTON_POSITIVE) as? MaterialButton?
+ if (positiveButton != null) {
+ util?.material?.colorMaterialButtonPrimaryTonal(positiveButton)
+ }
+
+ val negativeButton = alertDialog?.getButton(AlertDialog.BUTTON_NEGATIVE) as? MaterialButton?
+ if (negativeButton != null) {
+ util?.material?.colorMaterialButtonPrimaryBorderless(negativeButton)
+ }
+
+ val neutralButton = alertDialog?.getButton(AlertDialog.BUTTON_NEUTRAL) as? MaterialButton?
+ if (neutralButton != null) {
+ val warningColorId =
+ ContextCompat.getColor(requireContext(), R.color.highlight_textColor_Warning)
+ util?.platform?.colorTextButtons(warningColorId, neutralButton)
+ }
+
+ binding?.sharePasswordContainer?.let {
+ util?.material?.colorTextInputLayout(it)
+ }
+ }
+
+ companion object {
+ private const val ARG_FILE = "FILE"
+ private const val ARG_SHARE = "SHARE"
+ private const val ARG_CREATE_SHARE = "CREATE_SHARE"
+ private const val ARG_ASK_FOR_PASSWORD = "ASK_FOR_PASSWORD"
+ const val PASSWORD_FRAGMENT = "PASSWORD_FRAGMENT"
+
+ /**
+ * Public factory method to create new SharePasswordDialogFragment instances.
+ *
+ * @param note Note bound to the public share that which
+ * password will be set or updated
+ * @param createShare When 'true', the request for password will be
+ * followed by the creation of a new public link
+ * when 'false', a public share is assumed to exist, and the password is bound to it.
+ * @return Dialog ready to show.
+ */
+ @JvmStatic
+ fun newInstance(
+ note: Note?,
+ createShare: Boolean,
+ askForPassword: Boolean,
+ dialogListener: SharePasswordDialogListener
+ ): SharePasswordDialogFragment {
+ val bundle = Bundle().apply {
+ putSerializable(ARG_FILE, note)
+ putBoolean(ARG_CREATE_SHARE, createShare)
+ putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword)
+ }
+
+ return SharePasswordDialogFragment().apply {
+ listener = dialogListener
+ arguments = bundle
+ }
+ }
+
+ /**
+ * Public factory method to create new SharePasswordDialogFragment instances.
+ *
+ * @param share OCFile bound to the public share that which password will be set or updated
+ * @return Dialog ready to show.
+ */
+ @JvmStatic
+ fun newInstance(
+ share: OCShare?,
+ askForPassword: Boolean,
+ dialogListener: SharePasswordDialogListener
+ ): SharePasswordDialogFragment {
+ val bundle = Bundle().apply {
+ putParcelable(ARG_SHARE, share)
+ putBoolean(ARG_ASK_FOR_PASSWORD, askForPassword)
+ }
+
+ return SharePasswordDialogFragment().apply {
+ listener = dialogListener
+ arguments = bundle
+ }
+ }
+
+ /**
+ * Public factory method to create new SharePasswordDialogFragment instances.
+ *
+ * @param share OCFile bound to the public share that which password will be set or updated
+ * @return Dialog ready to show.
+ */
+ fun newInstance(
+ share: OCShare?,
+ dialogListener: SharePasswordDialogListener
+ ): SharePasswordDialogFragment {
+ val bundle = Bundle().apply {
+ putParcelable(ARG_SHARE, share)
+ }
+
+ return SharePasswordDialogFragment().apply {
+ listener = dialogListener
+ arguments = bundle
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/AvatarLoader.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/AvatarLoader.kt
new file mode 100644
index 000000000..3270b4056
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/AvatarLoader.kt
@@ -0,0 +1,34 @@
+package it.niedermann.owncloud.notes.share.helper
+
+import android.content.Context
+import android.net.Uri
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.request.RequestOptions
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.persistence.entity.Account
+
+object AvatarLoader {
+
+ fun load(context: Context, imageView: ImageView, account: Account) {
+ load(context, imageView, SingleSignOnUrl(account.accountName, account.avatarUrl))
+ }
+
+ fun load(context: Context, imageView: ImageView, account: Account, username: String) {
+ load(context, imageView, SingleSignOnUrl(account.accountName, getUrl(account.url, username)))
+ }
+
+ fun load(context: Context, imageView: ImageView, ssoURL: Any) {
+ Glide
+ .with(context)
+ .load(ssoURL)
+ .placeholder(R.drawable.ic_account_circle_grey_24dp)
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(imageView)
+ }
+
+ private fun getUrl(url: String, username: String): String =
+ url + "/index.php/avatar/" + Uri.encode(username) + "/64"
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java
new file mode 100644
index 000000000..f9723a9fd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java
@@ -0,0 +1,104 @@
+package it.niedermann.owncloud.notes.share.helper;
+
+
+import static com.owncloud.android.lib.resources.shares.OCShare.CREATE_PERMISSION_FLAG;
+import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FILE;
+import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER;
+import static com.owncloud.android.lib.resources.shares.OCShare.NO_PERMISSION;
+import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG;
+import static com.owncloud.android.lib.resources.shares.OCShare.SHARE_PERMISSION_FLAG;
+
+import android.content.Context;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import it.niedermann.owncloud.notes.R;
+
+/**
+ * Helper calls for visibility logic of the sharing menu.
+ */
+public final class SharingMenuHelper {
+
+ private SharingMenuHelper() {
+ // utility class -> private constructor
+ }
+
+ public static boolean isUploadAndEditingAllowed(OCShare share) {
+ if (share.getPermissions() == NO_PERMISSION) {
+ return false;
+ }
+
+ return (share.getPermissions() & (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER :
+ MAXIMUM_PERMISSIONS_FOR_FILE)) == (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER :
+ MAXIMUM_PERMISSIONS_FOR_FILE);
+ }
+
+ public static boolean isReadOnly(OCShare share) {
+ if (share.getPermissions() == NO_PERMISSION) {
+ return false;
+ }
+
+ return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == READ_PERMISSION_FLAG;
+ }
+
+ public static boolean isFileDrop(OCShare share) {
+ if (share.getPermissions() == NO_PERMISSION) {
+ return false;
+ }
+
+ return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == CREATE_PERMISSION_FLAG;
+ }
+
+ public static boolean isSecureFileDrop(OCShare share) {
+ if (share.getPermissions() == NO_PERMISSION) {
+ return false;
+ }
+
+ return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == CREATE_PERMISSION_FLAG + READ_PERMISSION_FLAG;
+ }
+
+ public static String getPermissionName(Context context, OCShare share) {
+ if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
+ return context.getResources().getString(R.string.share_permission_can_edit);
+ } else if (SharingMenuHelper.isReadOnly(share)) {
+ return context.getResources().getString(R.string.share_permission_view_only);
+ } else if (SharingMenuHelper.isSecureFileDrop(share)) {
+ return context.getResources().getString(R.string.share_permission_secure_file_drop);
+ } else if (SharingMenuHelper.isFileDrop(share)) {
+ return context.getResources().getString(R.string.share_permission_file_drop);
+ }
+ return null;
+ }
+
+ /**
+ * method to get the current checked index from the list of permissions
+ *
+ */
+ public static int getPermissionCheckedItem(Context context, OCShare share, String[] permissionArray) {
+ if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
+ if (share.isFolder()) {
+ return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_allow_upload_and_editing);
+ } else {
+ return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_editing);
+ }
+ } else if (SharingMenuHelper.isReadOnly(share)) {
+ return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_view_only);
+ } else if (SharingMenuHelper.isFileDrop(share)) {
+ return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_file_drop);
+ }
+ return 0;//default first item selected
+ }
+
+ private static int getPermissionIndexFromArray(Context context, String[] permissionArray, int permissionName) {
+ for (int i = 0; i < permissionArray.length; i++) {
+ if (permissionArray[i].equalsIgnoreCase(context.getResources().getString(permissionName))) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ public static boolean canReshare(OCShare share) {
+ return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/UsersAndGroupsSearchProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/UsersAndGroupsSearchProvider.java
new file mode 100644
index 000000000..462c65dd6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/UsersAndGroupsSearchProvider.java
@@ -0,0 +1,199 @@
+package it.niedermann.owncloud.notes.share.helper;
+
+
+import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_CLEAR_AT;
+import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_ICON;
+import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_MESSAGE;
+import static com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation.PROPERTY_STATUS;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation;
+import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.users.Status;
+import com.owncloud.android.lib.resources.users.StatusType;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+import java.util.Locale;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig;
+import it.niedermann.owncloud.notes.share.repository.ShareRepository;
+
+public class UsersAndGroupsSearchProvider {
+
+ public static final String SHARE_TYPE = "SHARE_TYPE";
+ public static final String SHARE_WITH = "SHARE_WITH";
+
+ private static final String[] COLUMNS = {
+ BaseColumns._ID,
+ SearchManager.SUGGEST_COLUMN_TEXT_1,
+ SearchManager.SUGGEST_COLUMN_TEXT_2,
+ SearchManager.SUGGEST_COLUMN_ICON_1,
+ SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+ SHARE_WITH,
+ SHARE_TYPE
+ };
+
+ private static final int SEARCH = 1;
+
+ private static final int RESULTS_PER_PAGE = 50;
+ private static final int REQUESTED_PAGE = 1;
+
+ public static final String CONTENT = "content";
+
+ private final String AUTHORITY;
+ private final String DATA_USER;
+ private final String DATA_GROUP;
+ private final String DATA_ROOM;
+ private final String DATA_REMOTE;
+ private final String DATA_EMAIL;
+ private final String DATA_CIRCLE;
+
+ private final ShareRepository repository;
+ private final Context context;
+
+ public UsersAndGroupsSearchProvider(Context context, ShareRepository repository) {
+ this.context = context;
+ this.repository = repository;
+
+ AUTHORITY = context.getString(R.string.users_and_groups_search_provider_authority);
+ DATA_USER = AUTHORITY + ".data.user";
+ DATA_GROUP = AUTHORITY + ".data.group";
+ DATA_ROOM = AUTHORITY + ".data.room";
+ DATA_REMOTE = AUTHORITY + ".data.remote";
+ DATA_EMAIL = AUTHORITY + ".data.email";
+ DATA_CIRCLE = AUTHORITY + ".data.circle";
+
+ UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mUriMatcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH);
+ }
+
+ public Cursor searchForUsersOrGroups(String userQuery, boolean isFederationShareAllowed) throws JSONException {
+ final var names = repository.getSharees(userQuery, REQUESTED_PAGE, RESULTS_PER_PAGE);
+ MatrixCursor response = null;
+ if (!names.isEmpty()) {
+ response = new MatrixCursor(COLUMNS);
+
+ Uri userBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_USER).build();
+ Uri groupBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_GROUP).build();
+ Uri roomBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_ROOM).build();
+ Uri remoteBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_REMOTE).build();
+ Uri emailBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_EMAIL).build();
+ Uri circleBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_CIRCLE).build();
+
+ Iterator namesIt = names.iterator();
+ JSONObject item;
+ String displayName;
+ String subline = null;
+ Object icon = 0;
+ Uri dataUri;
+ int count = 0;
+ while (namesIt.hasNext()) {
+ item = namesIt.next();
+ dataUri = null;
+ displayName = null;
+ String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL);
+ String name = item.isNull("name") ? "" : item.getString("name");
+ JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE);
+ ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE));
+ String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH);
+
+ Status status;
+ JSONObject statusObject = item.optJSONObject(PROPERTY_STATUS);
+
+ if (statusObject != null) {
+ status = new Status(
+ StatusType.valueOf(statusObject.getString(PROPERTY_STATUS).toUpperCase(Locale.US)),
+ statusObject.isNull(PROPERTY_MESSAGE) ? "" : statusObject.getString(PROPERTY_MESSAGE),
+ statusObject.isNull(PROPERTY_ICON) ? "" : statusObject.getString(PROPERTY_ICON),
+ statusObject.isNull(PROPERTY_CLEAR_AT) ? -1 : statusObject.getLong(PROPERTY_CLEAR_AT));
+ } else {
+ status = new Status(StatusType.OFFLINE, "", "", -1);
+ }
+
+ if (UsersAndGroupsSearchConfig.INSTANCE.getSearchOnlyUsers() && type != ShareType.USER) {
+ // skip all types but users, as E2E secure share is only allowed to users on same server
+ continue;
+ }
+
+ switch (type) {
+ case GROUP:
+ displayName = userName;
+ icon = R.drawable.ic_group;
+ dataUri = Uri.withAppendedPath(groupBaseUri, shareWith);
+ break;
+
+ case FEDERATED:
+ if (isFederationShareAllowed) {
+ icon = R.drawable.ic_account_circle_grey_24dp;
+ dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith);
+
+ if (userName.equals(shareWith)) {
+ displayName = name;
+ subline = context.getString(R.string.remote);
+ } else {
+ String[] uriSplitted = shareWith.split("@");
+ displayName = name;
+ subline = context.getString(R.string.share_known_remote_on_clarification,
+ uriSplitted[uriSplitted.length - 1]);
+ }
+ }
+ break;
+
+ case USER:
+ displayName = userName;
+ subline = (status.getMessage() == null || status.getMessage().isEmpty()) ? null :
+ status.getMessage();
+ icon = displayName;
+ dataUri = Uri.withAppendedPath(userBaseUri, shareWith);
+ break;
+
+ case EMAIL:
+ icon = R.drawable.ic_email;
+ displayName = name;
+ subline = shareWith;
+ dataUri = Uri.withAppendedPath(emailBaseUri, shareWith);
+ break;
+
+ case ROOM:
+ icon = R.drawable.ic_talk;
+ displayName = userName;
+ dataUri = Uri.withAppendedPath(roomBaseUri, shareWith);
+ break;
+
+ case CIRCLE:
+ icon = R.drawable.ic_circles;
+ displayName = userName;
+ dataUri = Uri.withAppendedPath(circleBaseUri, shareWith);
+ break;
+
+ default:
+ break;
+ }
+
+ if (displayName != null && dataUri != null) {
+ response.newRow()
+ .add(count++)
+ .add(displayName)
+ .add(subline)
+ .add(icon)
+ .add(dataUri)
+ .add(SHARE_WITH, shareWith)
+ .add(SHARE_TYPE, type.getValue());
+ }
+ }
+ }
+
+ return response;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/listener/NoteShareItemAction.java b/app/src/main/java/it/niedermann/owncloud/notes/share/listener/NoteShareItemAction.java
new file mode 100644
index 000000000..24fa5bad6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/listener/NoteShareItemAction.java
@@ -0,0 +1,32 @@
+package it.niedermann.owncloud.notes.share.listener;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+import com.owncloud.android.lib.resources.shares.ShareType;
+
+public interface NoteShareItemAction {
+
+ /**
+ * open advanced permission for selected share
+ */
+ void advancedPermissions(OCShare share);
+
+ /**
+ * open note screen to send new email
+ */
+ void sendNewEmail(OCShare share);
+
+ /**
+ * unshare the current share
+ */
+ void unShare(OCShare share);
+
+ /**
+ * send created link only valid for {@link ShareType#PUBLIC_LINK}
+ */
+ void sendLink(OCShare share);
+
+ /**
+ * create another link only valid for {@link ShareType#PUBLIC_LINK}
+ */
+ void addAnotherLink(OCShare share);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/listener/ShareeListAdapterListener.java b/app/src/main/java/it/niedermann/owncloud/notes/share/listener/ShareeListAdapterListener.java
new file mode 100644
index 000000000..c39d208f6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/listener/ShareeListAdapterListener.java
@@ -0,0 +1,23 @@
+package it.niedermann.owncloud.notes.share.listener;
+
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+public interface ShareeListAdapterListener {
+ void copyLink(OCShare share);
+
+ void showSharingMenuActionSheet(OCShare share);
+
+ void copyInternalLink();
+
+ void createPublicShareLink();
+
+ void createSecureFileDrop();
+
+ void requestPasswordForShare(OCShare share, boolean askForPassword);
+
+ void showPermissionsDialog(OCShare share);
+
+ void showProfileBottomSheet(Account account, String shareWith);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt
new file mode 100644
index 000000000..8ae313b81
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt
@@ -0,0 +1,27 @@
+package it.niedermann.owncloud.notes.share.model
+
+import com.google.gson.annotations.Expose
+
+
+data class CreateShareRequest(
+ @Expose
+ val path: String,
+
+ @Expose
+ val shareType: Int,
+
+ @Expose
+ val shareWith: String,
+
+ @Expose
+ val publicUpload: String,
+
+ @Expose
+ val password: String?,
+
+ @Expose
+ val permissions: Int?,
+
+ @Expose
+ val note: String?
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponse.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponse.kt
new file mode 100644
index 000000000..28aef0a38
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponse.kt
@@ -0,0 +1,137 @@
+package it.niedermann.owncloud.notes.share.model
+
+import com.google.gson.annotations.Expose
+import com.google.gson.annotations.SerializedName
+
+data class CreateShareResponse(
+ @Expose
+ val id: String,
+
+ @Expose
+ @SerializedName("share_type")
+ val shareType: Long,
+
+ @Expose
+ @SerializedName("uid_owner")
+ val uidOwner: String,
+
+ @Expose
+ @SerializedName("displayname_owner")
+ val displaynameOwner: String,
+
+ @Expose
+ val permissions: Long,
+
+ @Expose
+ @SerializedName("can_edit")
+ val canEdit: Boolean,
+
+ @Expose
+ @SerializedName("can_delete")
+ val canDelete: Boolean,
+
+ @Expose
+ val stime: Long,
+
+ @Expose
+ @SerializedName("uid_file_owner")
+ val uidFileOwner: String,
+
+ @Expose
+ val note: String,
+
+ @Expose
+ val label: String,
+
+ @Expose
+ @SerializedName("displayname_file_owner")
+ val displaynameFileOwner: String,
+
+ @Expose
+ val path: String,
+
+ @Expose
+ @SerializedName("item_type")
+ val itemType: String,
+
+ @Expose
+ @SerializedName("item_permissions")
+ val itemPermissions: Long,
+
+ @Expose
+ @SerializedName("is-mount-root")
+ val isMountRoot: Boolean,
+
+ @Expose
+ @SerializedName("mount-type")
+ val mountType: String,
+
+ @Expose
+ val mimetype: String,
+
+ @Expose
+ @SerializedName("has_preview")
+ val hasPreview: Boolean,
+
+ @Expose
+ @SerializedName("storage_id")
+ val storageId: String,
+
+ @Expose
+ val storage: Long,
+
+ @Expose
+ @SerializedName("item_source")
+ val itemSource: Long,
+
+ @Expose
+ @SerializedName("file_source")
+ val fileSource: Long,
+
+ @Expose
+ @SerializedName("file_parent")
+ val fileParent: Long,
+
+ @Expose
+ @SerializedName("file_target")
+ val fileTarget: String,
+
+ @Expose
+ @SerializedName("item_size")
+ val itemSize: Long,
+
+ @Expose
+ @SerializedName("item_mtime")
+ val itemMtime: Long,
+
+ @Expose
+ @SerializedName("share_with")
+ val shareWith: String,
+
+ @Expose
+ @SerializedName("share_with_displayname")
+ val shareWithDisplayname: String,
+
+ @Expose
+ @SerializedName("mail_send")
+ val mailSend: Long,
+
+ @Expose
+ @SerializedName("hide_download")
+ val hideDownload: Long,
+
+ @Expose
+ val attributes: Any?,
+
+ @Expose
+ @SerializedName("url")
+ val url: String?,
+
+ @Expose
+ @SerializedName("token")
+ val token: String?,
+
+ @Expose
+ @SerializedName("password")
+ val password: String?
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponseExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponseExtensions.kt
new file mode 100644
index 000000000..0eb71c21a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareResponseExtensions.kt
@@ -0,0 +1,37 @@
+package it.niedermann.owncloud.notes.share.model
+
+import com.owncloud.android.lib.resources.shares.OCShare
+import com.owncloud.android.lib.resources.shares.ShareType
+
+fun List.toOCShareList(): List {
+ return map { response ->
+ response.toOCShare()
+ }.filter { it.id != -1L }
+}
+
+fun CreateShareResponse.toOCShare(): OCShare {
+ val response = this
+
+ return OCShare().apply {
+ id = response.id.toLongOrNull() ?: -1L
+ fileSource = response.fileSource
+ itemSource = response.itemSource
+ shareType = ShareType.fromValue(response.shareType.toInt())
+ shareWith = response.shareWith
+ path = response.path
+ permissions = response.permissions.toInt()
+ sharedDate = response.stime
+ token = response.token
+ sharedWithDisplayName = response.shareWithDisplayname
+ isFolder = response.itemType == "folder"
+ userId = response.uidOwner
+ shareLink = response.url
+ isPasswordProtected = !response.password.isNullOrEmpty()
+ note = response.note
+ isHideFileDownload = (response.hideDownload == 1L)
+ label = response.label
+ isHasPreview = response.hasPreview
+ mimetype = response.mimetype
+ ownerDisplayName = response.displaynameOwner
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt
new file mode 100644
index 000000000..45e1ccf21
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt
@@ -0,0 +1,3 @@
+package it.niedermann.owncloud.notes.share.model
+
+data class QuickPermissionModel(val permissionName: String, val isSelected: Boolean)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/SharePasswordRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/SharePasswordRequest.kt
new file mode 100644
index 000000000..b752d79c4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/SharePasswordRequest.kt
@@ -0,0 +1,5 @@
+package it.niedermann.owncloud.notes.share.model
+
+data class SharePasswordRequest(
+ val password: String
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateSharePermissionRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateSharePermissionRequest.kt
new file mode 100644
index 000000000..99d91f924
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateSharePermissionRequest.kt
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.share.model
+
+import com.google.gson.annotations.Expose
+import com.google.gson.annotations.SerializedName
+
+data class UpdateSharePermissionRequest(
+ @Expose
+ @SerializedName("permissions") val permissions: Int? = null,
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt
new file mode 100644
index 000000000..e0b2a9628
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt
@@ -0,0 +1,44 @@
+package it.niedermann.owncloud.notes.share.model
+
+import com.google.gson.annotations.Expose
+
+data class UpdateShareRequest(
+ @Expose
+ val share_id: Int,
+
+ @Expose
+ val permissions: Int?,
+
+ @Expose
+ val password: String,
+
+ @Expose
+ val publicUpload: String,
+
+ @Expose
+ val expireDate: String?,
+
+ @Expose
+ val note: String,
+
+ /**
+ * Array of ShareAttributes data class in JSON format
+ */
+ @Expose
+ val attributes: String?,
+
+ @Expose
+ val sendMail: String
+)
+
+data class ShareAttributesV2(
+ var scope: String,
+ var key: String,
+ var value: Boolean
+)
+
+data class ShareAttributesV1(
+ var scope: String,
+ var key: String,
+ var enabled: Boolean
+)
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UsersAndGroupsSearchConfig.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UsersAndGroupsSearchConfig.kt
new file mode 100644
index 000000000..10adce634
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UsersAndGroupsSearchConfig.kt
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.share.model
+
+object UsersAndGroupsSearchConfig {
+ var searchOnlyUsers: Boolean = false
+
+ fun reset() {
+ searchOnlyUsers = false
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt
new file mode 100644
index 000000000..2328b1552
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt
@@ -0,0 +1,416 @@
+package it.niedermann.owncloud.notes.share.repository
+
+import android.content.Context
+import com.google.gson.Gson
+import com.google.gson.internal.LinkedTreeMap
+import com.nextcloud.android.sso.model.SingleSignOnAccount
+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.persistence.ApiProvider
+import it.niedermann.owncloud.notes.persistence.ApiResult
+import it.niedermann.owncloud.notes.persistence.NotesRepository
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.persistence.entity.ShareEntity
+import it.niedermann.owncloud.notes.share.model.CreateShareRequest
+import it.niedermann.owncloud.notes.share.model.CreateShareResponse
+import it.niedermann.owncloud.notes.share.model.ShareAttributesV1
+import it.niedermann.owncloud.notes.share.model.ShareAttributesV2
+import it.niedermann.owncloud.notes.share.model.SharePasswordRequest
+import it.niedermann.owncloud.notes.share.model.UpdateSharePermissionRequest
+import it.niedermann.owncloud.notes.share.model.UpdateShareRequest
+import it.niedermann.owncloud.notes.share.model.toOCShareList
+import it.niedermann.owncloud.notes.shared.model.ApiVersion
+import it.niedermann.owncloud.notes.shared.model.Capabilities
+import it.niedermann.owncloud.notes.shared.model.NotesSettings
+import it.niedermann.owncloud.notes.shared.model.OcsResponse
+import it.niedermann.owncloud.notes.shared.util.StringConstants
+import it.niedermann.owncloud.notes.shared.util.extensions.getErrorMessage
+import it.niedermann.owncloud.notes.shared.util.extensions.toExpirationDateString
+import org.json.JSONObject
+import java.util.Date
+
+class ShareRepository(
+ private val applicationContext: Context,
+ private val account: SingleSignOnAccount
+) {
+
+ private val tag = "ShareRepository"
+ private val gson = Gson()
+ private val apiProvider: ApiProvider by lazy { ApiProvider.getInstance() }
+ private val notesRepository: NotesRepository by lazy {
+ NotesRepository.getInstance(
+ applicationContext,
+ )
+ }
+
+ private fun getNotesPathResponseResult(): NotesSettings? {
+ val notesPathCall = notesRepository.getServerSettings(account, ApiVersion.API_VERSION_1_0)
+ val notesPathResponse = notesPathCall.execute()
+ return notesPathResponse.body()
+ }
+
+ private fun getNotePath(note: Note): String? {
+ val notesPathResponseResult = getNotesPathResponseResult() ?: return null
+ val notesPath = notesPathResponseResult.notesPath
+ val notesSuffix = notesPathResponseResult.fileSuffix
+ return StringConstants.PATH + notesPath + StringConstants.PATH + note.title + notesSuffix
+ }
+
+ fun getShareEntitiesForSpecificNote(note: Note): List {
+ val path = getNotePath(note)
+ return notesRepository.getShareEntities(path)
+ }
+
+ private fun getExpirationDate(chosenExpDateInMills: Long): String? {
+ if (chosenExpDateInMills == -1L) {
+ return null
+ }
+
+ return Date(chosenExpDateInMills).toExpirationDateString()
+ }
+
+ fun getCapabilities(): Capabilities = notesRepository.capabilities
+
+ // region API calls
+ fun getSharesForNotesAndSaveShareEntities() {
+ val notesPathResponseResult = getNotesPathResponseResult() ?: return
+ val notesPath = notesPathResponseResult.notesPath
+ val remotePath = "/$notesPath"
+
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.getSharesForSpecificNote(remotePath)
+ val entities = arrayListOf()
+
+ try {
+ if (call != null) {
+ val respOCS = call["ocs"] as? LinkedTreeMap<*, *>
+ val respData = respOCS?.getList("data")
+ respData?.forEach { data ->
+ val map = data as? LinkedTreeMap<*, *>
+ val id = map?.get("id") as? String
+ val note = map?.get("note") as? String
+ val path = map?.get("path") as? String
+ val fileTarget = map?.get("file_target") as? String
+ val shareWith = map?.get("share_with") as? String
+ val shareWithDisplayName = map?.get("share_with_displayname") as? String
+ val uidFileOwner = map?.get("uid_file_owner") as? String
+ val displayNameFileOwner = map?.get("displayname_file_owner") as? String
+ val uidOwner = map?.get("uid_owner") as? String
+ val displayNameOwner = map?.get("displayname_owner") as? String
+ val url = map?.get("url") as? String
+
+ id?.toInt()?.let {
+ val entity = ShareEntity(
+ id = it,
+ note = note,
+ path = path,
+ file_target = fileTarget,
+ share_with = shareWith,
+ share_with_displayname = shareWithDisplayName,
+ uid_file_owner = uidFileOwner,
+ displayname_file_owner = displayNameFileOwner,
+ uid_owner = uidOwner,
+ displayname_owner = displayNameOwner,
+ url = url
+ )
+
+ entities.add(entity)
+ }
+ }
+
+ notesRepository.addShareEntities(entities)
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while getSharesForSpecificNote: $e")
+ }
+ }
+
+ private fun LinkedTreeMap<*, *>.getList(key: String): ArrayList<*>? = this[key] as? ArrayList<*>
+
+ fun getSharees(
+ searchString: String,
+ page: Int,
+ perPage: Int
+ ): ArrayList {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.getSharees(
+ search = searchString,
+ page = page.toString(),
+ perPage = perPage.toString()
+ )
+ return if (call != null) {
+ val respOCS = call["ocs"] as? LinkedTreeMap<*, *>
+ val respData = respOCS?.get("data") as? LinkedTreeMap<*, *>
+ val respExact = respData?.get("exact") as? LinkedTreeMap<*, *>
+
+ val respExactUsers = respExact?.getList("users")
+ val respExactGroups = respExact?.getList("groups")
+ val respExactRemotes = respExact?.getList("remotes")
+ val respExactEmails = respExact?.getList("emails")
+ val respExactCircles =
+ respExact?.takeIf { it.containsKey("circles") }?.getList("circles")
+ val respExactRooms = respExact?.takeIf { it.containsKey("rooms") }?.getList("rooms")
+
+ val respPartialUsers = respData?.getList("users")
+ val respPartialGroups = respData?.getList("groups")
+ val respPartialRemotes = respData?.getList("remotes")
+ val respPartialCircles =
+ respData?.takeIf { it.containsKey("circles") }?.getList("circles")
+ val respPartialRooms = respData?.takeIf { it.containsKey("rooms") }?.getList("rooms")
+
+ val jsonResults = listOfNotNull(
+ respExactUsers,
+ respExactGroups,
+ respExactRemotes,
+ respExactRooms,
+ respExactEmails,
+ respExactCircles,
+ respPartialUsers,
+ respPartialGroups,
+ respPartialRemotes,
+ respPartialRooms,
+ respPartialCircles
+ )
+
+ return jsonResults.flatMap { jsonResult ->
+ jsonResult.map { linkedTreeMap ->
+ JSONObject(gson.toJson(linkedTreeMap))
+ }
+ }.toCollection(ArrayList())
+ } else {
+ ArrayList()
+ }
+ }
+
+ fun getUpdateShareRequest(
+ downloadPermission: Boolean,
+ share: OCShare?,
+ noteText: String,
+ password: String,
+ sendEmail: Boolean,
+ chosenExpDateInMills: Long,
+ permission: Int
+ ): UpdateShareRequest {
+ val capabilities = getCapabilities()
+ val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30
+
+ val shareAttributes = arrayOf(
+ if (shouldUseShareAttributesV2) {
+ ShareAttributesV2(
+ scope = "permissions",
+ key = "download",
+ value = downloadPermission
+ )
+ } else {
+ ShareAttributesV1(
+ scope = "permissions",
+ key = "download",
+ enabled = downloadPermission
+ )
+ }
+ )
+
+ val attributes = gson.toJson(shareAttributes)
+
+ return UpdateShareRequest(
+ share_id = share!!.id.toInt(),
+ permissions = if (permission == -1) null else permission,
+ password = password,
+ publicUpload = "false",
+ expireDate = getExpirationDate(chosenExpDateInMills),
+ note = noteText,
+ attributes = attributes,
+ sendMail = sendEmail.toString()
+ )
+ }
+
+ fun getShares(remoteId: Long): List? {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.getShares(remoteId)
+ val response = call.execute()
+
+ return try {
+ if (response.isSuccessful) {
+ val result =
+ response.body()?.ocs?.data ?: throw RuntimeException("No shares available")
+ result.toOCShareList()
+ } else {
+ Log_OC.d(tag, "Failed to getShares: ${response.errorBody()?.string()}")
+ null
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while getShares: $e")
+ null
+ }
+ }
+
+ fun sendEmail(shareId: Long, requestBody: SharePasswordRequest): Boolean {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.sendEmail(shareId, requestBody)
+ val response = call.execute()
+
+ return try {
+ if (response.isSuccessful) {
+ true
+ } else {
+ Log_OC.d(tag, "Failed to send-email: ${response.errorBody()?.string()}")
+ false
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while send-email: $e")
+ false
+ }
+ }
+
+ fun getShareFromNote(note: Note): List? {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val path = getNotePath(note) ?: return null
+ val call = shareAPI.getShareFromNote(path)
+ val response = call.execute()
+
+ return try {
+ if (response.isSuccessful) {
+ val body = response.body()
+ Log_OC.d(tag, "Response successful: $body")
+ body?.ocs?.data?.toOCShareList()
+ } else {
+ val errorBody = response.errorBody()?.string()
+ Log_OC.d(tag, "Response failed: $errorBody")
+ null
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while getting share from note: ", e)
+ null
+ }
+ }
+
+ fun removeShare(share: OCShare, note: Note): Boolean {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+
+ return try {
+ val call = shareAPI.removeShare(share.id)
+ val response = call.execute()
+ if (response.isSuccessful) {
+
+ if (share.shareType != null && share.shareType == ShareType.PUBLIC_LINK) {
+ note.setIsShared(false)
+ updateNote(note)
+ }
+
+ Log_OC.d(tag, "Share removed successfully.")
+ } else {
+ Log_OC.d(tag, "Failed to remove share: ${response.errorBody()?.string()}")
+ }
+ response.isSuccessful
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while removing share", e)
+ false
+ }
+ }
+
+ fun updateShare(
+ shareId: Long,
+ requestBody: UpdateShareRequest
+ ): ApiResult?> {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.updateShare(shareId, requestBody)
+ val response = call.execute()
+ return try {
+ if (response.isSuccessful) {
+ Log_OC.d(tag, "Share updated successfully: ${response.body().toString()}")
+ ApiResult.Success(
+ data = response.body(),
+ message = applicationContext.getString(R.string.note_share_created)
+ )
+ } else {
+ Log_OC.d(tag, "Failed to update share: ${response.errorBody()?.string()}")
+ ApiResult.Error(message = response.getErrorMessage() ?: "")
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while updating share", e)
+ ApiResult.Error(message = e.message ?: "")
+ }
+ }
+
+ fun updateNote(note: Note) = notesRepository.updateNote(note)
+
+ fun addShare(
+ note: Note,
+ shareType: ShareType,
+ shareWith: String,
+ publicUpload: String = "false",
+ password: String = "",
+ permissions: Int = 0,
+ shareNote: String = ""
+ ): ApiResult?> {
+ val defaultErrorMessage =
+ applicationContext.getString(R.string.note_share_activity_cannot_created)
+ val notesPathCall = notesRepository.getServerSettings(account, ApiVersion.API_VERSION_1_0)
+ val notesPathResponse = notesPathCall.execute()
+ val notesPathResponseResult =
+ notesPathResponse.body() ?: return ApiResult.Error(message = defaultErrorMessage)
+ val notesPath = notesPathResponseResult.notesPath
+ val notesSuffix = notesPathResponseResult.fileSuffix
+
+ val requestBody = CreateShareRequest(
+ path = StringConstants.PATH + notesPath + StringConstants.PATH + note.title + notesSuffix,
+ shareType = shareType.value,
+ shareWith = shareWith,
+ publicUpload = publicUpload,
+ password = password,
+ permissions = permissions,
+ note = shareNote
+ )
+
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val call = shareAPI.addShare(request = requestBody)
+ val response = call.execute()
+
+ return try {
+ if (response.isSuccessful) {
+ val createShareResponse = response.body()
+ Log_OC.d(tag, "Response successful: $createShareResponse")
+ ApiResult.Success(
+ data = createShareResponse,
+ message = applicationContext.getString(R.string.note_share_created)
+ )
+ } else {
+ val errorMessage = response.getErrorMessage()
+ if (errorMessage == null) {
+ return ApiResult.Error(message = defaultErrorMessage)
+ }
+ Log_OC.d(tag, "Response failed: $errorMessage")
+ ApiResult.Error(message = errorMessage)
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while creating share", e)
+ ApiResult.Error(message = defaultErrorMessage)
+ }
+ }
+
+ fun updateSharePermission(
+ shareId: Long,
+ permissions: Int? = null,
+ ): ApiResult?> {
+ val shareAPI = apiProvider.getShareAPI(applicationContext, account)
+ val requestBody = UpdateSharePermissionRequest(permissions = permissions)
+
+ return try {
+ val call = shareAPI.updateSharePermission(shareId, requestBody)
+ val response = call.execute()
+ if (response.isSuccessful) {
+ Log_OC.d(tag, "Share updated successfully: ${response.body()}")
+ ApiResult.Success(response.body())
+ } else {
+ Log_OC.d(tag, "Failed to update share: ${response.errorBody()?.string()}")
+ ApiResult.Error(message = response.getErrorMessage() ?: "", code = null)
+ }
+ } catch (e: Exception) {
+ Log_OC.d(tag, "Exception while updating share", e)
+ ApiResult.Error(message = e.message ?: "", code = null)
+ }
+ }
+ // endregion
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
index fbc04e79d..1804644ac 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java
@@ -11,13 +11,9 @@
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-
-import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
-import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
public class AccountChooserViewHolder extends RecyclerView.ViewHolder {
private final ItemAccountChooseBinding binding;
@@ -28,14 +24,7 @@ protected AccountChooserViewHolder(ItemAccountChooseBinding binding) {
}
public void bind(Account localAccount, Consumer targetAccountConsumer) {
- Glide
- .with(binding.accountItemAvatar.getContext())
- .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
- .placeholder(R.drawable.ic_account_circle_grey_24dp)
- .error(R.drawable.ic_account_circle_grey_24dp)
- .apply(RequestOptions.circleCropTransform())
- .into(binding.accountItemAvatar);
-
+ AvatarLoader.INSTANCE.load(binding.accountItemAvatar.getContext(), binding.accountItemAvatar, localAccount);
binding.accountLayout.setOnClickListener((v) -> targetAccountConsumer.accept(localAccount));
binding.accountName.setText(localAccount.getDisplayName());
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
index 806b94e90..e7f1f98d2 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java
@@ -10,8 +10,26 @@
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
-public class Capabilities {
+import com.owncloud.android.lib.resources.shares.OCShare;
+
+import java.io.Serializable;
+
+@Entity(tableName = "capabilities")
+public class Capabilities implements Serializable {
+ @PrimaryKey
+ public int id = 1;
+
+ /**
+ * 30(Major) .0(Minor). 5(Micro)
+ */
+ private String nextcloudMajorVersion = null;
+ private String nextcloudMinorVersion = null;
+ private String nextcloudMicroVersion = null;
+
+ private boolean federationShare = false;
private String apiVersion = null;
@ColorInt
@@ -23,6 +41,43 @@ public class Capabilities {
private boolean directEditingAvailable;
+ private boolean publicPasswordEnforced;
+ private boolean askForOptionalPassword;
+ private boolean isReSharingAllowed;
+ private int defaultPermission = OCShare.NO_PERMISSION;
+
+ public boolean isReSharingAllowed() {
+ return isReSharingAllowed;
+ }
+
+ public void setReSharingAllowed(boolean value) {
+ this.isReSharingAllowed = value;
+ }
+
+ public boolean getPublicPasswordEnforced() {
+ return publicPasswordEnforced;
+ }
+
+ public void setPublicPasswordEnforced(boolean value) {
+ this.publicPasswordEnforced = value;
+ }
+
+ public int getDefaultPermission() {
+ return defaultPermission;
+ }
+
+ public void setDefaultPermission(int value) {
+ this.defaultPermission = value;
+ }
+
+ public boolean getAskForOptionalPassword() {
+ return askForOptionalPassword;
+ }
+
+ public void setAskForOptionalPassword(boolean value) {
+ this.askForOptionalPassword = value;
+ }
+
public void setApiVersion(String apiVersion) {
this.apiVersion = apiVersion;
}
@@ -36,6 +91,41 @@ public String getETag() {
return eTag;
}
+ @Nullable
+ public String getNextcloudMajorVersion() {
+ return nextcloudMajorVersion;
+ }
+
+ @Nullable
+ public String getNextcloudMinorVersion() {
+ return nextcloudMinorVersion;
+ }
+
+ @Nullable
+ public String getNextcloudMicroVersion() {
+ return nextcloudMicroVersion;
+ }
+
+ public void setNextcloudMajorVersion(@Nullable String nextcloudMajorVersion) {
+ this.nextcloudMajorVersion = nextcloudMajorVersion;
+ }
+
+ public void setNextcloudMinorVersion(@Nullable String nextcloudMinorVersion) {
+ this.nextcloudMinorVersion = nextcloudMinorVersion;
+ }
+
+ public void setNextcloudMicroVersion(@Nullable String nextcloudMicroVersion) {
+ this.nextcloudMicroVersion = nextcloudMicroVersion;
+ }
+
+ public boolean getFederationShare() {
+ return federationShare;
+ }
+
+ public void setFederationShare(boolean value) {
+ this.federationShare = value;
+ }
+
public void setETag(@Nullable String eTag) {
this.eTag = eTag;
}
@@ -69,6 +159,12 @@ public void setDirectEditingAvailable(boolean directEditingAvailable) {
public String toString() {
return "Capabilities{" +
"apiVersion='" + apiVersion + '\'' +
+ ", nextcloudMajorVersion='" + nextcloudMajorVersion + '\'' +
+ ", nextcloudMinorVersion='" + nextcloudMinorVersion + '\'' +
+ ", nextcloudMicroVersion='" + nextcloudMicroVersion + '\'' +
+ ", federationShare=" + federationShare +
+ ", publicPasswordEnforced=" + publicPasswordEnforced +
+ ", askForOptionalPassword=" + askForOptionalPassword +
", color=" + color +
", textColor=" + textColor +
", eTag='" + eTag + '\'' +
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java
index e59939d47..a8c97a2ab 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java
@@ -10,18 +10,25 @@
import static java.util.Collections.singletonList;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Rect;
+import android.net.Uri;
import android.os.Build;
+import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.WindowInsets;
import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
import androidx.core.view.ViewCompat;
+import com.google.android.material.snackbar.Snackbar;
+
import java.util.Collection;
import java.util.List;
import java.util.Locale;
@@ -108,4 +115,78 @@ public static boolean isSoftKeyboardVisible(@NonNull View parentView) {
final int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top);
return heightDiff >= estimatedKeyboardHeight;
}
+
+ static public void startLinkIntent(Activity activity, @StringRes int link) {
+ startLinkIntent(activity, activity.getString(link));
+ }
+
+ static public void startLinkIntent(Activity activity, String url) {
+ if (!TextUtils.isEmpty(url)) {
+ startLinkIntent(activity, Uri.parse(url));
+ }
+ }
+
+ static public void startLinkIntent(Activity activity, Uri uri) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ DisplayUtils.startIntentIfAppAvailable(intent, activity, R.string.no_browser_available);
+ }
+
+ static public void startIntentIfAppAvailable(Intent intent, Activity activity, @StringRes int error) {
+ if (intent.resolveActivity(activity.getPackageManager()) != null) {
+ activity.startActivity(intent);
+ } else {
+ DisplayUtils.showSnackMessage(activity, error);
+ }
+ }
+
+ /**
+ * Show a temporary message in a {@link Snackbar} bound to the content view.
+ *
+ * @param activity The {@link Activity} to which's content view the {@link Snackbar} is bound.
+ * @param messageResource The resource id of the string resource to use. Can be formatted text.
+ * @return The created {@link Snackbar}
+ */
+ public static Snackbar showSnackMessage(Activity activity, @StringRes int messageResource) {
+ return showSnackMessage(activity.findViewById(android.R.id.content), messageResource);
+ }
+
+ /**
+ * Show a temporary message in a {@link Snackbar} bound to the content view.
+ *
+ * @param activity The {@link Activity} to which's content view the {@link Snackbar} is bound.
+ * @param message Message to show.
+ * @return The created {@link Snackbar}
+ */
+ public static Snackbar showSnackMessage(Activity activity, String message) {
+ final Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG);
+ snackbar.show();
+ return snackbar;
+ }
+
+ /**
+ * Show a temporary message in a {@link Snackbar} bound to the given view.
+ *
+ * @param view The view the {@link Snackbar} is bound to.
+ * @param messageResource The resource id of the string resource to use. Can be formatted text.
+ * @return The created {@link Snackbar}
+ */
+ public static Snackbar showSnackMessage(View view, @StringRes int messageResource) {
+ final Snackbar snackbar = Snackbar.make(view, messageResource, Snackbar.LENGTH_LONG);
+ snackbar.show();
+ return snackbar;
+ }
+
+ /**
+ * Show a temporary message in a {@link Snackbar} bound to the given view.
+ *
+ * @param view The view the {@link Snackbar} is bound to.
+ * @param message The message.
+ * @return The created {@link Snackbar}
+ */
+ public static Snackbar showSnackMessage(View view, String message) {
+ final Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
+ snackbar.show();
+ return snackbar;
+ }
+
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt
new file mode 100644
index 000000000..82a0977d2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt
@@ -0,0 +1,78 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2025 Alper Ozturk
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ */
+package it.niedermann.owncloud.notes.shared.util
+
+import android.content.Context
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
+import androidx.annotation.Px
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.graphics.BlendModeColorFilterCompat
+import androidx.core.graphics.BlendModeCompat
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.shares.ShareType
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+
+object FilesSpecificViewThemeUtils {
+
+ const val TAG = "FilesSpecificViewThemeUtils"
+
+ private object AvatarPadding {
+ @Px
+ const val SMALL = 4
+
+ @Px
+ const val LARGE = 8
+ }
+
+ fun createAvatar(type: ShareType?, avatar: ImageView, context: Context) {
+ fun createAvatarBase(@DrawableRes icon: Int, padding: Int = AvatarPadding.SMALL) {
+ avatar.setImageResource(icon)
+ avatar.background = ResourcesCompat.getDrawable(
+ context.resources,
+ R.drawable.round_bgnd,
+ null
+ )
+ avatar.cropToPadding = true
+ avatar.setPadding(padding, padding, padding, padding)
+ }
+
+ val androidViewThemeUtils = BrandingUtil.getInstance(context).platform
+
+ when (type) {
+ ShareType.GROUP -> {
+ createAvatarBase(R.drawable.ic_group)
+ androidViewThemeUtils.colorImageViewBackgroundAndIcon(avatar)
+ }
+ ShareType.ROOM -> {
+ createAvatarBase(R.drawable.first_run_talk, AvatarPadding.LARGE)
+ androidViewThemeUtils.colorImageViewBackgroundAndIcon(avatar)
+ }
+ ShareType.CIRCLE -> {
+ createAvatarBase(R.drawable.ic_circles)
+
+ val backgroundColor = ContextCompat.getColor(context, R.color.nc_grey)
+ avatar.background.colorFilter =
+ BlendModeColorFilterCompat
+ .createBlendModeColorFilterCompat(backgroundColor, BlendModeCompat.SRC_IN)
+
+ val foregroundColor = ContextCompat.getColor(context, R.color.icon_on_nc_grey)
+ avatar.drawable.mutate().colorFilter =
+ BlendModeColorFilterCompat
+ .createBlendModeColorFilterCompat(foregroundColor, BlendModeCompat.SRC_IN)
+ }
+ ShareType.EMAIL -> {
+ createAvatarBase(R.drawable.ic_email, AvatarPadding.LARGE)
+ androidViewThemeUtils.colorImageViewBackgroundAndIcon(avatar)
+ }
+ else -> Log_OC.d(TAG, "Unknown share type")
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/StringConstants.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/StringConstants.kt
new file mode 100644
index 000000000..305da7ca4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/StringConstants.kt
@@ -0,0 +1,5 @@
+package it.niedermann.owncloud.notes.shared.util
+
+object StringConstants {
+ const val PATH = "/"
+}
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
new file mode 100644
index 000000000..9e07f56e0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/ClipboardUtil.kt
@@ -0,0 +1,50 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2018 Andy Scherzinger
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ */
+
+package it.niedermann.owncloud.notes.shared.util.clipboard
+
+import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.text.TextUtils
+import android.widget.Toast
+import com.owncloud.android.lib.common.utils.Log_OC
+import it.niedermann.owncloud.notes.R
+
+/**
+ * Helper implementation to copy a string into the system clipboard.
+ */
+object ClipboardUtil {
+ private val TAG = ClipboardUtil::class.java.name
+
+ @JvmStatic
+ @JvmOverloads
+ @Suppress("TooGenericExceptionCaught")
+ fun copyToClipboard(activity: Activity, text: String?, showToast: Boolean = true) {
+ if (!TextUtils.isEmpty(text)) {
+ try {
+ val clip = ClipData.newPlainText(
+ activity.getString(
+ R.string.clipboard_label,
+ activity.getString(R.string.app_name)
+ ),
+ text
+ )
+ (activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip)
+ if (showToast) {
+ Toast.makeText(activity, R.string.clipboard_text_copied, Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: Exception) {
+ Toast.makeText(activity, 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()
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/CopyToClipboardActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/CopyToClipboardActivity.kt
new file mode 100644
index 000000000..4c5ed9587
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/clipboard/CopyToClipboardActivity.kt
@@ -0,0 +1,16 @@
+package it.niedermann.owncloud.notes.shared.util.clipboard
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+
+/**
+ * Activity copying the text of the received Intent into the system clipboard.
+ */
+class CopyToClipboardActivity : Activity() {
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ ClipboardUtil.copyToClipboard(this, intent.getCharSequenceExtra(Intent.EXTRA_TEXT).toString())
+ finish()
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/BundleExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/BundleExtensions.kt
new file mode 100644
index 000000000..9fb4c9e78
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/BundleExtensions.kt
@@ -0,0 +1,54 @@
+/*
+ * Nextcloud - Android Client
+ *
+ * SPDX-FileCopyrightText: 2023 Alper Ozturk
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH
+ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
+ */
+package it.niedermann.owncloud.notes.shared.util.extensions
+
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import java.io.Serializable
+
+@Suppress("TopLevelPropertyNaming")
+
+fun Bundle?.getSerializableArgument(key: String, type: Class): T? {
+ if (this == null) {
+ return null
+ }
+
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this.getSerializable(key, type)
+ } else {
+ @Suppress("UNCHECKED_CAST", "DEPRECATION")
+ if (type.isInstance(this.getSerializable(key))) {
+ this.getSerializable(key) as T
+ } else {
+ null
+ }
+ }
+ } catch (e: ClassCastException) {
+ null
+ }
+}
+
+fun Bundle?.getParcelableArgument(key: String, type: Class): T? {
+ if (this == null) {
+ return null
+ }
+
+ return try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ this.getParcelable(key, type)
+ } else {
+ @Suppress("DEPRECATION")
+ this.getParcelable(key)
+ }
+ } catch (e: ClassCastException) {
+ e.printStackTrace()
+ null
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/CallExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/CallExtensions.kt
new file mode 100644
index 000000000..46c40e711
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/CallExtensions.kt
@@ -0,0 +1,17 @@
+package it.niedermann.owncloud.notes.shared.util.extensions
+
+import it.niedermann.owncloud.notes.shared.model.OcsResponse
+import org.json.JSONObject
+import retrofit2.Response
+
+fun Response>.getErrorMessage(): String? {
+ if (isSuccessful) {
+ return null
+ }
+
+ val errorBody = errorBody()?.string() ?: return null
+ val jsonObject = JSONObject(errorBody)
+ return jsonObject.getJSONObject("ocs")
+ .getJSONObject("meta")
+ .getString("message")
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/DateExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/DateExtensions.kt
new file mode 100644
index 000000000..3426b180a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/extensions/DateExtensions.kt
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.shared.util.extensions
+
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+fun Date.toExpirationDateString(): String {
+ return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(this)
+}
diff --git a/app/src/main/res/drawable-night/round_bgnd.xml b/app/src/main/res/drawable-night/round_bgnd.xml
new file mode 100644
index 000000000..44d973897
--- /dev/null
+++ b/app/src/main/res/drawable-night/round_bgnd.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-night/shared_via_link.xml b/app/src/main/res/drawable-night/shared_via_link.xml
new file mode 100644
index 000000000..bd1841046
--- /dev/null
+++ b/app/src/main/res/drawable-night/shared_via_link.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml
new file mode 100644
index 000000000..b2172e676
--- /dev/null
+++ b/app/src/main/res/drawable/divider.xml
@@ -0,0 +1,19 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/file_calendar.xml b/app/src/main/res/drawable/file_calendar.xml
new file mode 100644
index 000000000..2fdb050e8
--- /dev/null
+++ b/app/src/main/res/drawable/file_calendar.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/first_run_talk.xml b/app/src/main/res/drawable/first_run_talk.xml
new file mode 100644
index 000000000..b2ed96f57
--- /dev/null
+++ b/app/src/main/res/drawable/first_run_talk.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml
new file mode 100644
index 000000000..5aee27252
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_check_24.xml b/app/src/main/res/drawable/ic_baseline_check_24.xml
new file mode 100644
index 000000000..c11536bf9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_check_24.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_circles.xml b/app/src/main/res/drawable/ic_circles.xml
new file mode 100644
index 000000000..3f152e7da
--- /dev/null
+++ b/app/src/main/res/drawable/ic_circles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_contact_book.xml b/app/src/main/res/drawable/ic_contact_book.xml
new file mode 100644
index 000000000..1ca1a2799
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contact_book.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml
new file mode 100644
index 000000000..a2b6c8e58
--- /dev/null
+++ b/app/src/main/res/drawable/ic_content_copy.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 000000000..0f6e822c9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dots_vertical.xml b/app/src/main/res/drawable/ic_dots_vertical.xml
new file mode 100644
index 000000000..5c2616239
--- /dev/null
+++ b/app/src/main/res/drawable/ic_dots_vertical.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml
new file mode 100644
index 000000000..9126482d2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_edit.xml
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_email.xml b/app/src/main/res/drawable/ic_email.xml
new file mode 100644
index 000000000..c6e736593
--- /dev/null
+++ b/app/src/main/res/drawable/ic_email.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_external.xml b/app/src/main/res/drawable/ic_external.xml
new file mode 100644
index 000000000..599123616
--- /dev/null
+++ b/app/src/main/res/drawable/ic_external.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_group.xml b/app/src/main/res/drawable/ic_group.xml
new file mode 100644
index 000000000..aad10ce3f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_group.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml
new file mode 100644
index 000000000..216121712
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml
new file mode 100644
index 000000000..4f0bc1a41
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plus.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_talk.xml b/app/src/main/res/drawable/ic_talk.xml
new file mode 100644
index 000000000..8957c64bd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_talk.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/round_bgnd.xml b/app/src/main/res/drawable/round_bgnd.xml
new file mode 100644
index 000000000..8c21e85d5
--- /dev/null
+++ b/app/src/main/res/drawable/round_bgnd.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/shared_via_link.xml b/app/src/main/res/drawable/shared_via_link.xml
new file mode 100644
index 000000000..d5ca0a47e
--- /dev/null
+++ b/app/src/main/res/drawable/shared_via_link.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_note_share.xml b/app/src/main/res/layout/activity_note_share.xml
new file mode 100644
index 000000000..ae4b8d5e0
--- /dev/null
+++ b/app/src/main/res/layout/activity_note_share.xml
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_note_share_detail.xml b/app/src/main/res/layout/activity_note_share_detail.xml
new file mode 100644
index 000000000..f412c1d13
--- /dev/null
+++ b/app/src/main/res/layout/activity_note_share_detail.xml
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_row.xml b/app/src/main/res/layout/activity_row.xml
new file mode 100644
index 000000000..ffb8e2e6c
--- /dev/null
+++ b/app/src/main/res/layout/activity_row.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_add_public_share.xml b/app/src/main/res/layout/item_add_public_share.xml
new file mode 100644
index 000000000..c31635628
--- /dev/null
+++ b/app/src/main/res/layout/item_add_public_share.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_internal_share_link.xml b/app/src/main/res/layout/item_internal_share_link.xml
new file mode 100644
index 000000000..50157edff
--- /dev/null
+++ b/app/src/main/res/layout/item_internal_share_link.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_note_share.xml b/app/src/main/res/layout/item_note_share.xml
new file mode 100644
index 000000000..d0423af00
--- /dev/null
+++ b/app/src/main/res/layout/item_note_share.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_note_share_action.xml b/app/src/main/res/layout/item_note_share_action.xml
new file mode 100644
index 000000000..64f3d2604
--- /dev/null
+++ b/app/src/main/res/layout/item_note_share_action.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 5b842046b..77c1ef076 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
@@ -95,7 +95,7 @@
android:padding="@dimen/spacer_1hx"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/secondary_font_size"
- app:chipBackgroundColor="@color/primary"
+ app:chipBackgroundColor="@color/defaultBrand"
app:chipEndPadding="@dimen/spacer_1x"
app:chipMinHeight="0dp"
app:chipStartPadding="@dimen/spacer_1x"
diff --git a/app/src/main/res/layout/item_quick_share_permissions.xml b/app/src/main/res/layout/item_quick_share_permissions.xml
new file mode 100644
index 000000000..bd2cf72ef
--- /dev/null
+++ b/app/src/main/res/layout/item_quick_share_permissions.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_share_link_share.xml b/app/src/main/res/layout/item_share_link_share.xml
new file mode 100644
index 000000000..0d9b9d833
--- /dev/null
+++ b/app/src/main/res/layout/item_share_link_share.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_share_share.xml b/app/src/main/res/layout/item_share_share.xml
new file mode 100644
index 000000000..108afe34e
--- /dev/null
+++ b/app/src/main/res/layout/item_share_share.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_suggestion_adapter.xml b/app/src/main/res/layout/item_suggestion_adapter.xml
new file mode 100644
index 000000000..bf73e1bbc
--- /dev/null
+++ b/app/src/main/res/layout/item_suggestion_adapter.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/password_dialog.xml b/app/src/main/res/layout/password_dialog.xml
new file mode 100644
index 000000000..c93e7acbb
--- /dev/null
+++ b/app/src/main/res/layout/password_dialog.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/profile_bottom_sheet_action.xml b/app/src/main/res/layout/profile_bottom_sheet_action.xml
new file mode 100644
index 000000000..06c5e60be
--- /dev/null
+++ b/app/src/main/res/layout/profile_bottom_sheet_action.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/profile_bottom_sheet_fragment.xml b/app/src/main/res/layout/profile_bottom_sheet_fragment.xml
new file mode 100644
index 000000000..38660f9a4
--- /dev/null
+++ b/app/src/main/res/layout/profile_bottom_sheet_fragment.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml b/app/src/main/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml
new file mode 100644
index 000000000..ba3d23e8f
--- /dev/null
+++ b/app/src/main/res/layout/quick_sharing_permissions_bottom_sheet_fragment.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 4200f876f..707ba348e 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -13,9 +13,12 @@
#121212
#f5f5f5
+ #E3E3E3
#eee5e5ee
#55eeeeff
+ #A5A5A5
+ #737373
#2a2a2a
@color/primary
@@ -32,4 +35,7 @@
#dd000000
#d8d8d8
+
+ #222222
+ #ffffff
\ 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 ce764c365..1a688fe65 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -14,8 +14,13 @@
#ffffff
#121212
#d40000
+ #333333
+ #616161
+ #FFFFFF
+ #e53935
#0082C9
+ #666666
#555566
#2233334a
@@ -30,7 +35,8 @@
#cccccc
#999999
#ffffff
-
+ #ededed
+ #000000
#757575
@color/grey600
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 5daaa6b03..c2c7d4080 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -9,6 +9,8 @@
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
+ 0dp
+
2dp
4dp
@@ -17,11 +19,25 @@
24dp
36dp
40dp
+ 140dp
+ 24dp
+ 180dp
+ 100dp
36dp
+ 196dip
+ 48dp
+ 40dp
+ 16sp
+ 56dp
+ 24dp
+ 28dp
+
0dp
+ 40dp
+
100dp
42dp
@@ -33,6 +49,7 @@
14sp
+ 12sp
16sp
18sp
22sp
@@ -48,4 +65,8 @@
4dp
26dp
20dp
+
+ 48dp
+
+ 56dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b596e5f7d..919f5fe49 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -32,12 +32,143 @@
Favorite
Preview
Share
+ Loading…
+
Search in %1$s
Search all notes
Choose a category
+
+ com.nextcloud.android.providers.UsersAndGroupsSearchProvider
+ com.nextcloud.android.providers.UsersAndGroupsSearchProvider.action.SHARE_WITH
+
+
+
+ - @string/link_share_view_only
+ - @string/link_share_allow_upload_and_editing
+ - @string/link_share_file_drop
+
+
+ - @string/link_share_view_only
+ - @string/link_share_editing
+
+
+
+ note activity icon
+ note share icon
+ note share external icon
+ note share copy icon
+ note share more icon
+ note share icon
+ note share user icon
+ note share contact icon
+ Shared with you by %1$s
+ Share note
+ Name, Federated Cloud ID or email address…
+ Share link
+ Policy or permissions prevent resharing
+ Could not retrieve URL
+ \"%1$s\" has been shared with you
+ Contact permission is required.
+ You are not allowed to create a public share.
+
+ Share created
+ You are not allowed to create a share.
+
+
+
+ Advanced Settings
+ Hide download
+ Note to recipient
+ Note
+ Next
+ Share and Copy Link
+ Confirm
+ Set Note
+ Send Share
+ Name
+ Link Name
+ You must enter a password
+ Password
+ Please select at least one permission to share.
+ Label cannot be empty
+ Cancel
+ Failed to create a share
+
+
+ Enter an optional password
+ Skip
+
+ Enter a password
+ OK
+ Delete
+ Send email
+ No app available to handle links
+ No App available to handle mail address
+ No actions for this user
+
+ Failed to remove share
+ Failed to pick email address.
+ Failed to update UI
+ No app available to select contacts
+ Send link to…
+
+ Send
+ Internal share link only works for users with access to this folder
+ Internal share link only works for users with access to this file
+ Share internal link
+ Delete Link
+ Settings
+ Send new email
+ Sharing
+ Share %1$s
+ Expires %1$s
+ %1$s
+ Set expiration date
+ Share link
+ Send link
+ Password-protected
+ Set password
+ Share with…
+ Unset
+ Add another link
+ Add new public share link
+ New name
+ Share link (%1$s)
+ Share link
+ Allow resharing
+ View only
+ Editing
+ Allow upload and editing
+ File drop (upload only)
+ Could not retrieve shares
+ View only
+ Can edit
+ File drop
+ Secure file drop
+ Share Permissions
+
+ - %1$d download remaining
+ - %1$d downloads remaining
+
+ Username
+ %1$s (group)
+ %1$s (remote)
+ %1$s (conversation)
+ on %1$s
+ (remote)
+
+
+
+ Copy link
+ Share
+ Link copied
+ Received no text to copy to clipboard
+ Unexpected error while copying to clipboard
+ Text copied from %1$s
+
Today
Yesterday
This week
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 36c07e913..6720f1725 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -35,6 +35,21 @@
- @style/tabStyle
+
+
+
+
+
+
+