Skip to content

Commit 4e096b4

Browse files
authored
Support direct editing for notes (#1686)
* feat(capabilities): Fetch and store direct editing capability using existing capabilities code Signed-off-by: Álvaro Brey <[email protected]> * feat: Implement direct editing repo Signed-off-by: Álvaro Brey <[email protected]> * wip: Edit note with webview Signed-off-by: Álvaro Brey <[email protected]> * wip: allow switching between the three note opening modes in preferences Signed-off-by: Álvaro Brey <[email protected]> * EditNoteActivity: if no setting, use plain edit, not direct edit Required by UX team Signed-off-by: Álvaro Brey <[email protected]> * feat: Add FAB to switch to rich editing mode from plain edit/preview Signed-off-by: Álvaro Brey <[email protected]> * feat: add fab while direct editing to switch to normal editing Signed-off-by: Álvaro Brey <[email protected]> * Fix toolbar when switching between direct edit and normal edit Signed-off-by: Álvaro Brey <[email protected]> * wip: error and conflict handling when switching edit modes Signed-off-by: Álvaro Brey <[email protected]> * Only show direct editing FAB if direct editing is available Signed-off-by: Álvaro Brey <[email protected]> * EditNoteActivity: if pref is direct edit but it's not available, launch normal edit instead Signed-off-by: Álvaro Brey <[email protected]> * Show error if direct editing not loaded after 10 seconds Signed-off-by: Álvaro Brey <[email protected]> * Update user agent for Notes webview Signed-off-by: Álvaro Brey <[email protected]> * Support opening new notes with direct editing Signed-off-by: Álvaro Brey <[email protected]> * Allow invalid ssl certs for debug builds in webview Development only! Signed-off-by: Álvaro Brey <[email protected]> * NoteDirectEdit: prevent duplicate note creation when creating it with direct edit remote id needs to be set Signed-off-by: Álvaro Brey <[email protected]> * Fix create with plain edit -> direct edit flow Signed-off-by: Álvaro Brey <[email protected]> --------- Signed-off-by: Álvaro Brey <[email protected]>
1 parent 83c91cc commit 4e096b4

37 files changed

+1156
-98
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ dependencies {
115115

116116
// ReactiveX
117117
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
118+
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
118119

119120
// Testing
120121
testImplementation 'androidx.test:core:1.5.0'

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import android.app.Application;
66
import android.content.Context;
77
import android.util.Log;
8+
import android.webkit.WebView;
89

910
import androidx.appcompat.app.AppCompatDelegate;
1011
import androidx.preference.PreferenceManager;
@@ -29,6 +30,9 @@ public void onCreate() {
2930
lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false);
3031
isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false);
3132
super.onCreate();
33+
if (BuildConfig.DEBUG) {
34+
WebView.setWebContentsDebuggingEnabled(true);
35+
}
3236
}
3337

3438
public static void setAppTheme(DarkModeSetting setting) {

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
7171
private Note originalNote;
7272
private int originalScrollY;
7373
protected NotesRepository repo;
74-
private NoteFragmentListener listener;
74+
@Nullable
75+
protected NoteFragmentListener listener;
7576
private boolean titleModified = false;
7677

7778
protected boolean isNew = true;
@@ -143,6 +144,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
143144
@Nullable
144145
protected abstract ScrollView getScrollView();
145146

147+
146148
protected abstract void scrollToY(int scrollY);
147149

148150
@Override
@@ -240,7 +242,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
240242
.show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()));
241243
return true;
242244
} else if (itemId == R.id.menu_share) {
243-
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
245+
shareNote();
244246
return false;
245247
} else if (itemId == MENU_ID_PIN) {
246248
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -263,6 +265,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
263265
return super.onOptionsItemSelected(item);
264266
}
265267

268+
protected void shareNote() {
269+
ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent());
270+
}
271+
266272
@CallSuper
267273
protected void onNoteLoaded(Note note) {
268274
this.originalScrollY = note.getScrollY();
@@ -273,10 +279,21 @@ protected void onNoteLoaded(Note note) {
273279
if (scrollY > 0) {
274280
note.setScrollY(scrollY);
275281
}
282+
onScroll(scrollY, oldScrollY);
276283
});
277284
}
278285
}
279286

287+
/**
288+
* Scroll callback, to be overridden by subclasses. Default implementation is empty
289+
*/
290+
protected void onScroll(int scrollY, int oldScrollY) {
291+
}
292+
293+
protected boolean shouldShowToolbar() {
294+
return true;
295+
}
296+
280297
public void onCloseNote() {
281298
if (!titleModified && originalNote == null && getContent().isEmpty()) {
282299
repo.deleteNoteAndSync(localAccount, note.getId());
@@ -367,8 +384,14 @@ public void moveNote(Account account) {
367384
}
368385

369386
public interface NoteFragmentListener {
387+
enum Mode {
388+
EDIT, PREVIEW, DIRECT_EDIT
389+
}
390+
370391
void close();
371392

372393
void onNoteUpdated(Note note);
394+
395+
void changeMode(@NonNull Mode mode, boolean reloadNote);
373396
}
374397
}

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

Lines changed: 139 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
import android.util.Log;
99
import android.view.Menu;
1010
import android.view.MenuItem;
11+
import android.view.View;
1112
import android.view.WindowManager;
1213
import android.widget.Toast;
1314

1415
import androidx.annotation.NonNull;
16+
import androidx.annotation.Nullable;
1517
import androidx.fragment.app.Fragment;
1618
import androidx.lifecycle.ViewModelProvider;
1719
import androidx.preference.PreferenceManager;
1820

1921
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
2022
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
2123
import com.nextcloud.android.sso.helper.SingleAccountHelper;
24+
import com.nextcloud.android.sso.model.SingleSignOnAccount;
2225

2326
import java.io.BufferedReader;
2427
import java.io.IOException;
@@ -34,6 +37,7 @@
3437
import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
3538
import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
3639
import it.niedermann.owncloud.notes.main.MainActivity;
40+
import it.niedermann.owncloud.notes.persistence.NotesRepository;
3741
import it.niedermann.owncloud.notes.persistence.entity.Account;
3842
import it.niedermann.owncloud.notes.persistence.entity.Note;
3943
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
@@ -57,11 +61,14 @@ public class EditNoteActivity extends LockedActivity implements BaseNoteFragment
5761
private ActivityEditBinding binding;
5862

5963
private BaseNoteFragment fragment;
64+
private NotesRepository repo;
6065

6166
@Override
6267
protected void onCreate(final Bundle savedInstanceState) {
6368
super.onCreate(savedInstanceState);
6469

70+
repo = NotesRepository.getInstance(getApplicationContext());
71+
6572
try {
6673
if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) == null) {
6774
throw new NoCurrentAccountSelectedException();
@@ -118,9 +125,20 @@ private long getNoteId() {
118125
}
119126

120127
private long getAccountId() {
121-
return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
128+
final long idParam = getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
129+
if (idParam == 0) {
130+
try {
131+
final SingleSignOnAccount ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(this);
132+
return repo.getAccountByName(ssoAcc.name).getId();
133+
} catch (NextcloudFilesAppAccountNotFoundException |
134+
NoCurrentAccountSelectedException e) {
135+
Log.w(TAG, "getAccountId: no current account", e);
136+
}
137+
}
138+
return idParam;
122139
}
123140

141+
124142
/**
125143
* Starts the note fragment for an existing note or a new note.
126144
* The actual behavior is triggered by the activity's intent.
@@ -145,44 +163,109 @@ private void launchNoteFragment() {
145163
* @param noteId ID of the existing note.
146164
*/
147165
private void launchExistingNote(long accountId, long noteId) {
148-
final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
149-
final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
150-
final var prefValueEdit = getString(R.string.pref_value_mode_edit);
151-
final var prefValuePreview = getString(R.string.pref_value_mode_preview);
152-
final var prefValueLast = getString(R.string.pref_value_mode_last);
166+
launchExistingNote(accountId, noteId, null);
167+
}
153168

154-
final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
155-
final String mode = preferences.getString(prefKeyNoteMode, prefValueEdit);
156-
final String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit);
157-
boolean editMode = true;
158-
if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) {
159-
editMode = false;
160-
}
161-
launchExistingNote(accountId, noteId, editMode);
169+
private void launchExistingNote(long accountId, long noteId, @Nullable final String mode) {
170+
launchExistingNote(accountId, noteId, mode, false);
162171
}
163172

164173
/**
165174
* Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note.
166175
*
167-
* @param noteId ID of the existing note.
168-
* @param edit View-mode of the fragment:
169-
* <code>true</code> for {@link NoteEditFragment},
170-
* <code>false</code> for {@link NotePreviewFragment}.
176+
* @param noteId ID of the existing note.
177+
* @param mode View-mode of the fragment (pref value or null). If null will be chosen based on
178+
* user preferences.
179+
* @param discardState If true, the state of the fragment will be discarded and a new fragment will be created
171180
*/
172-
private void launchExistingNote(long accountId, long noteId, boolean edit) {
181+
private void launchExistingNote(long accountId, long noteId, @Nullable final String mode, final boolean discardState) {
173182
// save state of the fragment in order to resume with the same note and originalNote
174-
Fragment.SavedState savedState = null;
175-
if (fragment != null) {
176-
savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
183+
runOnUiThread(() -> {
184+
Fragment.SavedState savedState = null;
185+
if (fragment != null && !discardState) {
186+
savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
187+
}
188+
fragment = getNoteFragment(accountId, noteId, mode);
189+
if (savedState != null) {
190+
fragment.setInitialSavedState(savedState);
191+
}
192+
replaceFragment();
193+
});
194+
}
195+
196+
private void replaceFragment() {
197+
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
198+
if (!fragment.shouldShowToolbar()) {
199+
binding.toolbar.setVisibility(View.GONE);
200+
} else {
201+
binding.toolbar.setVisibility(View.VISIBLE);
177202
}
178-
fragment = edit
179-
? NoteEditFragment.newInstance(accountId, noteId)
180-
: NotePreviewFragment.newInstance(accountId, noteId);
203+
}
204+
205+
206+
/**
207+
* Returns the preferred mode for the account. If the mode is "remember last" the last mode is returned.
208+
* If the mode is "direct edit" and the account does not support direct edit, the default mode is returned.
209+
*/
210+
private String getPreferenceMode(long accountId) {
211+
212+
final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
213+
final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
214+
final var defaultMode = getString(R.string.pref_value_mode_edit);
215+
final var prefValueLast = getString(R.string.pref_value_mode_last);
216+
final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
181217

182-
if (savedState != null) {
183-
fragment.setInitialSavedState(savedState);
218+
219+
final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
220+
final String modePreference = preferences.getString(prefKeyNoteMode, defaultMode);
221+
222+
String effectiveMode = modePreference;
223+
if (modePreference.equals(prefValueLast)) {
224+
effectiveMode = preferences.getString(prefKeyLastMode, defaultMode);
225+
}
226+
227+
if (effectiveMode.equals(prefValueDirectEdit)) {
228+
final Account accountById = repo.getAccountById(accountId);
229+
final var directEditAvailable = accountById != null && accountById.isDirectEditingAvailable();
230+
if (!directEditAvailable) {
231+
effectiveMode = defaultMode;
232+
}
233+
}
234+
235+
return effectiveMode;
236+
}
237+
238+
private BaseNoteFragment getNoteFragment(long accountId, long noteId, final @Nullable String modePref) {
239+
240+
final var effectiveMode = modePref == null ? getPreferenceMode(accountId) : modePref;
241+
242+
final var prefValueEdit = getString(R.string.pref_value_mode_edit);
243+
final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
244+
final var prefValuePreview = getString(R.string.pref_value_mode_preview);
245+
246+
if (effectiveMode.equals(prefValueEdit)) {
247+
return NoteEditFragment.newInstance(accountId, noteId);
248+
} else if (effectiveMode.equals(prefValueDirectEdit)) {
249+
return NoteDirectEditFragment.newInstance(accountId, noteId);
250+
} else if (effectiveMode.equals(prefValuePreview)) {
251+
return NotePreviewFragment.newInstance(accountId, noteId);
252+
} else {
253+
throw new IllegalStateException("Unknown note modePref: " + modePref);
254+
}
255+
}
256+
257+
258+
@NonNull
259+
private BaseNoteFragment getNewNoteFragment(Note newNote) {
260+
final var mode = getPreferenceMode(getAccountId());
261+
262+
final var prefValueDirectEdit = getString(R.string.pref_value_mode_direct_edit);
263+
264+
if (mode.equals(prefValueDirectEdit)) {
265+
return NoteDirectEditFragment.newInstanceWithNewNote(newNote);
266+
} else {
267+
return NoteEditFragment.newInstanceWithNewNote(newNote);
184268
}
185-
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
186269
}
187270

188271
/**
@@ -219,10 +302,11 @@ private void launchNewNote() {
219302
content = "";
220303
}
221304
final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
222-
fragment = NoteEditFragment.newInstanceWithNewNote(newNote);
223-
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
305+
fragment = getNewNoteFragment(newNote);
306+
replaceFragment();
224307
}
225308

309+
226310
private void launchReadonlyNote() {
227311
final var intent = getIntent();
228312
final var content = new StringBuilder();
@@ -238,7 +322,7 @@ private void launchReadonlyNote() {
238322
}
239323

240324
fragment = NoteReadonlyFragment.newInstance(content.toString());
241-
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
325+
replaceFragment();
242326
}
243327

244328
@Override
@@ -260,10 +344,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
260344
close();
261345
return true;
262346
} else if (itemId == R.id.menu_preview) {
263-
launchExistingNote(getAccountId(), getNoteId(), false);
347+
changeMode(Mode.PREVIEW, false);
264348
return true;
265349
} else if (itemId == R.id.menu_edit) {
266-
launchExistingNote(getAccountId(), getNoteId(), true);
350+
changeMode(Mode.EDIT, false);
267351
return true;
268352
}
269353
return super.onOptionsItemSelected(item);
@@ -281,8 +365,10 @@ public void close() {
281365
final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
282366
if (fragment instanceof NoteEditFragment) {
283367
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply();
284-
} else {
368+
} else if (fragment instanceof NotePreviewFragment) {
285369
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply();
370+
} else if (fragment instanceof NoteDirectEditFragment) {
371+
preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_direct_edit)).apply();
286372
}
287373
fragment.onCloseNote();
288374

@@ -308,6 +394,24 @@ public void onNoteUpdated(Note note) {
308394
}
309395
}
310396

397+
@Override
398+
public void changeMode(@NonNull Mode mode, boolean reloadNote) {
399+
switch (mode) {
400+
case EDIT:
401+
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_edit), reloadNote);
402+
break;
403+
case PREVIEW:
404+
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_preview), reloadNote);
405+
break;
406+
case DIRECT_EDIT:
407+
launchExistingNote(getAccountId(), getNoteId(), getString(R.string.pref_value_mode_direct_edit), reloadNote);
408+
break;
409+
default:
410+
throw new IllegalStateException("Unknown mode: " + mode);
411+
}
412+
}
413+
414+
311415
@Override
312416
public void onAccountPicked(@NonNull Account account) {
313417
fragment.moveNote(account);
@@ -318,4 +422,4 @@ public void applyBrand(int color) {
318422
final var util = BrandingUtil.of(color, this);
319423
util.notes.applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar, colorAccent);
320424
}
321-
}
425+
}

0 commit comments

Comments
 (0)