Skip to content

Commit dc91638

Browse files
Merge pull request #2574 from nextcloud/feature/internal-share
Feature - Share Functionality
2 parents efc44f6 + 1af63df commit dc91638

File tree

108 files changed

+6756
-78
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+6756
-78
lines changed

app/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ ext {
9595
dependencies {
9696
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
9797

98+
implementation 'com.google.guava:guava:31.1-android'
99+
implementation ('commons-httpclient:commons-httpclient:3.1') {
100+
exclude group: 'commons-logging', module: 'commons-logging'
101+
}
102+
103+
implementation("com.github.nextcloud:android-library:2.19.0") {
104+
exclude group: 'org.ogce', module: 'xpp3'
105+
}
106+
98107
// Nextcloud SSO
99108
implementation 'com.github.nextcloud.android-common:ui:48ed8e86d9'
100109
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.2'

app/src/main/AndroidManifest.xml

+14-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
<uses-permission android:name="android.permission.INTERNET" />
1313
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
14-
14+
<uses-permission android:name="android.permission.READ_CONTACTS" />
15+
<uses-permission android:name="com.owncloud.android.providers.PERMISSION" />
1516
<queries>
1617
<package android:name="com.nextcloud.client" />
1718
<package android:name="com.nextcloud.android.beta" />
@@ -47,10 +48,22 @@
4748
android:value=".android.activity.NotesListViewActivity" />
4849
</activity>
4950

51+
<activity
52+
android:name=".share.NoteShareActivity" />
53+
54+
<activity
55+
android:name=".share.NoteShareDetailActivity" />
56+
5057
<activity
5158
android:name=".importaccount.ImportAccountActivity"
5259
android:label="@string/add_account" />
5360

61+
<activity
62+
android:name=".shared.util.clipboard.CopyToClipboardActivity"
63+
android:exported="false"
64+
android:icon="@drawable/shared_via_link"
65+
android:label="@string/copy_link" />
66+
5467
<activity
5568
android:name=".AppendToNoteActivity"
5669
android:label="@string/append_to_note"

app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java

+2-7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
import androidx.annotation.NonNull;
1919
import androidx.fragment.app.DialogFragment;
2020

21-
import com.bumptech.glide.Glide;
22-
import com.bumptech.glide.request.RequestOptions;
2321
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
2422

2523
import it.niedermann.owncloud.notes.NotesApplication;
@@ -30,6 +28,7 @@
3028
import it.niedermann.owncloud.notes.manageaccounts.ManageAccountsActivity;
3129
import it.niedermann.owncloud.notes.persistence.NotesRepository;
3230
import it.niedermann.owncloud.notes.persistence.entity.Account;
31+
import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
3332

3433
/**
3534
* Displays all available {@link Account} entries and provides basic operations for them, like adding or switching
@@ -74,11 +73,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) {
7473

7574
binding.accountName.setText(currentLocalAccount.getDisplayName());
7675
binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost());
77-
Glide.with(requireContext())
78-
.load(currentLocalAccount.getUrl() + "/index.php/avatar/" + Uri.encode(currentLocalAccount.getUserName()) + "/64")
79-
.error(R.drawable.ic_account_circle_grey_24dp)
80-
.apply(RequestOptions.circleCropTransform())
81-
.into(binding.currentAccountItemAvatar);
76+
AvatarLoader.INSTANCE.load(requireContext(), binding.currentAccountItemAvatar, currentLocalAccount);
8277
binding.accountLayout.setOnClickListener((v) -> dismiss());
8378

8479
final var adapter = new AccountSwitcherAdapter((localAccount -> {

app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java

+2-11
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@
1414
import androidx.core.util.Consumer;
1515
import androidx.recyclerview.widget.RecyclerView;
1616

17-
import com.bumptech.glide.Glide;
18-
import com.bumptech.glide.request.RequestOptions;
19-
20-
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
21-
import it.niedermann.owncloud.notes.R;
2217
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
2318
import it.niedermann.owncloud.notes.persistence.entity.Account;
19+
import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
2420

2521
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
2622

@@ -34,12 +30,7 @@ public AccountSwitcherViewHolder(@NonNull View itemView) {
3430
public void bind(@NonNull Account localAccount, @NonNull Consumer<Account> onAccountClick) {
3531
binding.accountName.setText(localAccount.getDisplayName());
3632
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
37-
Glide.with(itemView.getContext())
38-
.load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
39-
.placeholder(R.drawable.ic_account_circle_grey_24dp)
40-
.error(R.drawable.ic_account_circle_grey_24dp)
41-
.apply(RequestOptions.circleCropTransform())
42-
.into(binding.accountItemAvatar);
33+
AvatarLoader.INSTANCE.load(itemView.getContext(), binding.accountItemAvatar, localAccount);
4334
itemView.setOnClickListener((v) -> onAccountClick.accept(localAccount));
4435
binding.accountContextMenu.setVisibility(View.GONE);
4536
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package it.niedermann.owncloud.notes.branding
2+
3+
import android.content.Context
4+
import androidx.annotation.ColorInt
5+
import com.google.android.material.bottomsheet.BottomSheetDialog
6+
7+
abstract class BrandedBottomSheetDialog(context: Context) : BottomSheetDialog(context), Branded {
8+
9+
override fun onStart() {
10+
super.onStart()
11+
12+
@ColorInt val color = BrandingUtil.readBrandMainColor(context)
13+
applyBrand(color)
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package it.niedermann.owncloud.notes.branding
2+
3+
import android.view.View
4+
import androidx.annotation.ColorInt
5+
import androidx.recyclerview.widget.RecyclerView
6+
7+
abstract class BrandedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), Branded {
8+
9+
fun bindBranding() {
10+
@ColorInt val color = BrandingUtil.readBrandMainColor(itemView.context)
11+
applyBrand(color)
12+
}
13+
}

app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
122122
if (content == null) {
123123
throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing.");
124124
} else {
125-
note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0);
125+
note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0, false, false);
126126
requireActivity().runOnUiThread(() -> onNoteLoaded(note));
127127
requireActivity().invalidateOptionsMenu();
128128
}

app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ private void launchNewNote() {
302302
if (content == null) {
303303
content = "";
304304
}
305-
final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
305+
final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null, false, false);
306306
fragment = getNewNoteFragment(newNote);
307307
replaceFragment();
308308
}

app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker;
100100
import it.niedermann.owncloud.notes.persistence.entity.Account;
101101
import it.niedermann.owncloud.notes.persistence.entity.Note;
102+
import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
102103
import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
103104
import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
104105
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
@@ -517,7 +518,7 @@ public void onError(@NonNull Throwable t) {
517518
public void onSelectionChanged() {
518519
super.onSelectionChanged();
519520
if (tracker.hasSelection() && mActionMode == null) {
520-
mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this, coordinatorLayout, binding.activityNotesListView.fabCreate, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
521+
mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this,MainActivity.this, coordinatorLayout, binding.activityNotesListView.fabCreate, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
521522
}
522523
if (mActionMode != null) {
523524
if (tracker.hasSelection()) {

app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java

+27-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
package it.niedermann.owncloud.notes.main;
88

99
import android.content.Context;
10+
import android.content.Intent;
11+
import android.os.Bundle;
1012
import android.util.TypedValue;
1113
import android.view.Menu;
1214
import android.view.MenuItem;
@@ -31,6 +33,7 @@
3133
import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment;
3234
import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
3335
import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
36+
import it.niedermann.owncloud.notes.share.NoteShareActivity;
3437
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
3538

3639
public class MultiSelectedActionModeCallback implements Callback {
@@ -53,8 +56,11 @@ public class MultiSelectedActionModeCallback implements Callback {
5356
private final SelectionTracker<Long> tracker;
5457
@NonNull
5558
private final FragmentManager fragmentManager;
59+
@NonNull
60+
private final MainActivity mainActivity;
5661

5762
public MultiSelectedActionModeCallback(
63+
@NonNull MainActivity mainActivity,
5864
@NonNull Context context,
5965
@NonNull View view,
6066
@NonNull View anchorView,
@@ -63,6 +69,7 @@ public MultiSelectedActionModeCallback(
6369
boolean canMoveNoteToAnotherAccounts,
6470
@NonNull SelectionTracker<Long> tracker,
6571
@NonNull FragmentManager fragmentManager) {
72+
this.mainActivity = mainActivity;
6673
this.context = context;
6774
this.view = view;
6875
this.anchorView = anchorView;
@@ -153,16 +160,26 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
153160
}
154161
tracker.clearSelection();
155162

156-
executor.submit(() -> {
157-
if (selection.size() == 1) {
158-
final var note = mainViewModel.getFullNote(selection.get(0));
159-
ShareUtil.openShareDialog(context, note.getTitle(), note.getContent());
160-
} else {
161-
ShareUtil.openShareDialog(context,
162-
context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
163-
mainViewModel.collectNoteContents(selection));
164-
}
165-
});
163+
if (selection.size() == 1) {
164+
final var currentAccount$ = mainViewModel.getCurrentAccount();
165+
currentAccount$.observe(lifecycleOwner, account -> {
166+
currentAccount$.removeObservers(lifecycleOwner);
167+
executor.submit(() -> {{
168+
final var note = mainViewModel.getFullNote(selection.get(0));
169+
Bundle bundle = new Bundle();
170+
bundle.putSerializable(NoteShareActivity.ARG_NOTE, note);
171+
bundle.putSerializable(NoteShareActivity.ARG_ACCOUNT, account);
172+
Intent intent = new Intent(mainActivity, NoteShareActivity.class);
173+
intent.putExtras(bundle);
174+
mainActivity.startActivity(intent);
175+
}});
176+
});
177+
} else {
178+
ShareUtil.openShareDialog(context,
179+
context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
180+
mainViewModel.collectNoteContents(selection));
181+
}
182+
166183
return true;
167184
} else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it
168185
final var accountLiveData = mainViewModel.getCurrentAccount();

app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountViewHolder.java

+2-9
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@
1818
import androidx.appcompat.widget.PopupMenu;
1919
import androidx.recyclerview.widget.RecyclerView;
2020

21-
import com.bumptech.glide.Glide;
22-
import com.bumptech.glide.request.RequestOptions;
23-
24-
import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
2521
import it.niedermann.owncloud.notes.R;
2622
import it.niedermann.owncloud.notes.branding.BrandingUtil;
2723
import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
2824
import it.niedermann.owncloud.notes.persistence.entity.Account;
25+
import it.niedermann.owncloud.notes.share.helper.AvatarLoader;
2926

3027
public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
3128

@@ -43,11 +40,7 @@ public void bind(
4340
) {
4441
binding.accountName.setText(localAccount.getUserName());
4542
binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
46-
Glide.with(itemView.getContext())
47-
.load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
48-
.error(R.drawable.ic_account_circle_grey_24dp)
49-
.apply(RequestOptions.circleCropTransform())
50-
.into(binding.accountItemAvatar);
43+
AvatarLoader.INSTANCE.load(itemView.getContext(), binding.accountItemAvatar, localAccount);
5144
itemView.setOnClickListener((v) -> callback.onSelect(localAccount));
5245
binding.accountContextMenu.setVisibility(VISIBLE);
5346
binding.accountContextMenu.setOnClickListener((v) -> {

app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.gson.JsonDeserializer;
1919
import com.google.gson.JsonPrimitive;
2020
import com.google.gson.JsonSerializer;
21+
import com.google.gson.Strictness;
2122
import com.nextcloud.android.sso.api.NextcloudAPI;
2223
import com.nextcloud.android.sso.model.SingleSignOnAccount;
2324

@@ -29,8 +30,10 @@
2930
import it.niedermann.owncloud.notes.persistence.sync.FilesAPI;
3031
import it.niedermann.owncloud.notes.persistence.sync.NotesAPI;
3132
import it.niedermann.owncloud.notes.persistence.sync.OcsAPI;
33+
import it.niedermann.owncloud.notes.persistence.sync.ShareAPI;
3234
import it.niedermann.owncloud.notes.shared.model.ApiVersion;
3335
import it.niedermann.owncloud.notes.shared.model.Capabilities;
36+
import okhttp3.ResponseBody;
3437
import retrofit2.NextcloudRetrofitApiBuilder;
3538
import retrofit2.Retrofit;
3639

@@ -47,12 +50,14 @@ public class ApiProvider {
4750

4851
private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/";
4952
private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/";
53+
private static final String API_ENDPOINT_FILES_SHARING ="/ocs/v2.php/apps/files_sharing/api/v1/";
5054

5155
private static final Map<String, NextcloudAPI> API_CACHE = new ConcurrentHashMap<>();
5256

5357
private static final Map<String, OcsAPI> API_CACHE_OCS = new ConcurrentHashMap<>();
5458
private static final Map<String, NotesAPI> API_CACHE_NOTES = new ConcurrentHashMap<>();
5559
private static final Map<String, FilesAPI> API_CACHE_FILES = new ConcurrentHashMap<>();
60+
private static final Map<String, ShareAPI> API_CACHE_FILES_SHARING = new ConcurrentHashMap<>();
5661

5762

5863
public static ApiProvider getInstance() {
@@ -96,13 +101,23 @@ public synchronized FilesAPI getFilesAPI(@NonNull Context context, @NonNull Sing
96101
return filesAPI;
97102
}
98103

104+
public synchronized ShareAPI getShareAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
105+
if (API_CACHE_FILES_SHARING.containsKey(ssoAccount.name)) {
106+
return API_CACHE_FILES_SHARING.get(ssoAccount.name);
107+
}
108+
final var shareAPI = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_ENDPOINT_FILES_SHARING).create(ShareAPI.class);
109+
API_CACHE_FILES_SHARING.put(ssoAccount.name, shareAPI);
110+
return shareAPI;
111+
}
112+
99113
private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
100114
if (API_CACHE.containsKey(ssoAccount.name)) {
101115
return API_CACHE.get(ssoAccount.name);
102116
} else {
103117
Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
104118
final var nextcloudAPI = new NextcloudAPI(context.getApplicationContext(), ssoAccount,
105119
new GsonBuilder()
120+
.setStrictness(Strictness.LENIENT)
106121
.excludeFieldsWithoutExposeAnnotation()
107122
.registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer<Calendar>) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.getTimeInMillis() / 1_000))
108123
.registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer<Calendar>) (src, typeOfSrc, ctx) -> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package it.niedermann.owncloud.notes.persistence
2+
3+
sealed class ApiResult<out T> {
4+
data class Success<out T>(val data: T, val message: String? = null) : ApiResult<T>()
5+
data class Error(val message: String, val code: Int? = null) : ApiResult<Nothing>()
6+
}
7+
8+
fun <T> ApiResult<T>.isSuccess(): Boolean = this is ApiResult.Success<T>
9+
fun <T> ApiResult<T>.isError(): Boolean = this is ApiResult.Error

app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public Result doWork() {
5252
for (final var account : repo.getAccounts()) {
5353
try {
5454
final var ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName());
55+
5556
Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
5657
final var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag(), ApiProvider.getInstance());
5758
repo.updateCapabilitiesETag(account.getId(), capabilities.getETag());

0 commit comments

Comments
 (0)