Merge branch 'multi-account' of github.com:stefan-niedermann/nextcloud-notes into multi-account.rebase-test.test.2

Signed-off-by: Daniel Bailey <daniel.bailey@grappleIT.co.uk>
This commit is contained in:
Daniel Bailey 2019-10-11 15:31:14 +01:00
commit 7e82e3da1f
No known key found for this signature in database
GPG key ID: CFF0B83BD28D4CE0
23 changed files with 439 additions and 293 deletions

View file

@ -1,16 +1,19 @@
package it.niedermann.owncloud.notes.android;
import android.content.Context;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.WindowManager;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
/**
* Extension of the {@link AppCompatAutoCompleteTextView}, but this one is always open, i.e. you can see the list of suggestions even the TextView is empty.
*/
public class AlwaysAutoCompleteTextView extends AppCompatAutoCompleteTextView {
private static final String TAG = AlwaysAutoCompleteTextView.class.getSimpleName();
private int myThreshold;
public AlwaysAutoCompleteTextView(Context context) {
@ -50,7 +53,7 @@ public class AlwaysAutoCompleteTextView extends AppCompatAutoCompleteTextView {
} catch (WindowManager.BadTokenException e) {
// https://github.com/stefan-niedermann/nextcloud-notes/issues/366
e.printStackTrace();
Log.e(AlwaysAutoCompleteTextView.class.getSimpleName(), "Exception", e);
Log.e(TAG, "Exception", e);
}
}
}

View file

@ -2,6 +2,7 @@ package it.niedermann.owncloud.notes.android.activity;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -37,26 +38,23 @@ public class AboutActivity extends AppCompatActivity {
}
private class TabsPagerAdapter extends FragmentPagerAdapter {
private final int PAGE_COUNT = 3;
public TabsPagerAdapter(FragmentManager fragmentManager) {
TabsPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public int getCount() {
return PAGE_COUNT;
return 3;
}
/**
* return the right fragment for the given position
*/
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new AboutFragmentCreditsTab();
case 1:
return new AboutFragmentContributingTab();
@ -64,7 +62,7 @@ public class AboutActivity extends AppCompatActivity {
return new AboutFragmentLicenseTab();
default:
return null;
return new AboutFragmentCreditsTab();
}
}

View file

@ -13,6 +13,7 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Calendar;
import java.util.Objects;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.android.fragment.BaseNoteFragment;
@ -26,6 +27,8 @@ import it.niedermann.owncloud.notes.util.NoteUtil;
public class EditNoteActivity extends AppCompatActivity implements BaseNoteFragment.NoteFragmentListener {
private static final String TAG = EditNoteActivity.class.getSimpleName();
public static final String ACTION_SHORTCUT = "it.niedermann.owncloud.notes.shortcut";
private static final String INTENT_GOOGLE_ASSISTANT = "com.google.android.gm.action.AUTO_SEND";
private static final String MIMETYPE_TEXT_PLAIN = "text/plain";
@ -54,7 +57,7 @@ public class EditNoteActivity extends AppCompatActivity implements BaseNoteFragm
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(getClass().getSimpleName(), "onNewIntent: " + intent.getLongExtra(PARAM_NOTE_ID, 0));
Log.d(TAG, "onNewIntent: " + intent.getLongExtra(PARAM_NOTE_ID, 0));
setIntent(intent);
if (fragment != null) {
getFragmentManager().beginTransaction().detach(fragment).commit();
@ -211,12 +214,17 @@ public class EditNoteActivity extends AppCompatActivity implements BaseNoteFragm
@Override
public void onNoteUpdated(DBNote note) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
ActionBar actionBar = Objects.requireNonNull(getSupportActionBar());
if (note != null) {
actionBar.setTitle(note.getTitle());
if (!note.getCategory().isEmpty()) {
actionBar.setSubtitle(NoteUtil.extendCategory(note.getCategory()));
}
} else {
// Maybe account is not authenticated -> note == null
Log.e(TAG, "note is null, start NotesListViewActivity");
startActivity(new Intent(this, NotesListViewActivity.class));
finish();
}
}
}

View file

@ -1,5 +1,6 @@
package it.niedermann.owncloud.notes.android.activity;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.os.Bundle;
@ -30,6 +31,7 @@ public class ExceptionActivity extends AppCompatActivity {
public static final String KEY_THROWABLE = "T";
@SuppressLint("SetTextI18n") // only used for logging
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.activity_exception);

View file

@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteConstraintException;
import android.graphics.Canvas;
import android.graphics.drawable.Icon;
import android.net.Uri;
@ -30,6 +31,7 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.recyclerview.widget.ItemTouchHelper;
@ -48,8 +50,6 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -69,12 +69,15 @@ import it.niedermann.owncloud.notes.util.ExceptionHandler;
import it.niedermann.owncloud.notes.util.ICallback;
import it.niedermann.owncloud.notes.util.NoteUtil;
import it.niedermann.owncloud.notes.util.NotesClientUtil;
import it.niedermann.owncloud.notes.util.SSOUtil;
import static it.niedermann.owncloud.notes.android.activity.EditNoteActivity.ACTION_SHORTCUT;
import static it.niedermann.owncloud.notes.util.SSOUtil.askForNewAccount;
public class NotesListViewActivity extends AppCompatActivity implements ItemAdapter.NoteClickListener {
private static final String TAG = NotesListViewActivity.class.getSimpleName();
public static final String CREATED_NOTE = "it.niedermann.owncloud.notes.created_notes";
public static final String ADAPTER_KEY_RECENT = "recent";
public static final String ADAPTER_KEY_STARRED = "starred";
@ -92,6 +95,8 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
private LocalAccount localAccount;
@BindView(R.id.coordinatorLayout)
CoordinatorLayout coordinatorLayout;
@BindView(R.id.accountNavigation)
LinearLayout accountNavigation;
@BindView(R.id.accountChooser)
@ -152,7 +157,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
.setIntent(intent)
.build());
}
Log.d(getClass().getSimpleName(), "Update dynamic shortcuts");
Log.d(TAG, "Update dynamic shortcuts");
shortcutManager.removeAllDynamicShortcuts();
shortcutManager.addDynamicShortcuts(newShortcuts);
}
@ -228,19 +233,6 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
handleNotAuthorizedAccount();
}
try { // to get current account from SingleAccountHelper
selectAccount(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
Log.v(getClass().getSimpleName(), "NextcloudRequest account: " + localAccount);
} catch (NextcloudFilesAppAccountNotFoundException e) {
e.printStackTrace();
} catch (NoCurrentAccountSelectedException e) {
if (db.hasAccounts()) { // If nothing is stored in SingleAccountHelper, check db for accounts
selectAccount(db.getAccounts().get(0).getAccountName());
} else {
askForNewAccount(this);
}
}
// refresh and sync every time the activity gets
if (localAccount != null) {
refreshLists();
@ -275,16 +267,32 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
}
private void selectAccount(String accountName) {
fabCreate.hide();
SingleAccountHelper.setCurrentAccount(getApplicationContext(), accountName);
localAccount = db.getLocalAccountByAccountName(accountName);
db.getNoteServerSyncHelper().updateAccount();
synchronize();
refreshLists();
try {
db.getNoteServerSyncHelper().updateAccount();
synchronize();
refreshLists();
fabCreate.show();
} catch (NextcloudFilesAppAccountNotFoundException e) {
handleNotAuthorizedAccount();
}
setupHeader();
setupNavigationList(ADAPTER_KEY_RECENT);
updateUsernameInDrawer();
}
private void handleNotAuthorizedAccount() {
fabCreate.hide();
swipeRefreshLayout.setRefreshing(false);
if (db.hasAccounts()) {
SSOUtil.authorizeExistingAccount(this, db.getAccounts().get(0).getAccountName());
} else {
askForNewAccount(this);
}
}
private void setupHeader() {
accountChooser.removeAllViews();
for (LocalAccount account : db.getAccounts()) {
@ -297,9 +305,9 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
.apply(RequestOptions.circleCropTransform())
.into(((ImageView) v.findViewById(R.id.accountItemAvatar)));
v.setOnClickListener(clickedView -> {
selectAccount(account.getAccountName());
clickHeader();
drawerLayout.closeDrawer(GravityCompat.START);
selectAccount(account.getAccountName());
});
v.findViewById(R.id.delete).setOnClickListener(clickedView -> {
db.deleteAccount(account.getId());
@ -400,7 +408,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
// update views
if (closeNavigation) {
drawerLayout.closeDrawers();
drawerLayout.closeDrawer(GravityCompat.START);
}
refreshLists(true);
}
@ -437,7 +445,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
private class LoadCategoryListTask extends AsyncTask<Void, Void, List<NavigationAdapter.NavigationItem>> {
@Override
protected List<NavigationAdapter.NavigationItem> doInBackground(Void... voids) {
if(localAccount == null) {
if (localAccount == null) {
return new ArrayList<>();
}
List<NavigationAdapter.NavigationItem> categories = db.getCategories(localAccount.getId());
@ -591,7 +599,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
db.deleteNoteAndSync((dbNote).getId());
adapter.remove(dbNote);
refreshLists();
Log.v(getClass().getSimpleName(), "Item deleted through swipe ----------------------------------------------");
Log.v(TAG, "Item deleted through swipe ----------------------------------------------");
Snackbar.make(swipeRefreshLayout, R.string.action_note_deleted, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo, (View v) -> {
db.addNoteAndSync(dbNote.getAccountId(), dbNote);
@ -703,9 +711,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
if (currentVisibility == View.VISIBLE) {
fabCreate.hide();
} else {
new Handler().postDelayed(() -> {
fabCreate.show();
}, 150);
new Handler().postDelayed(() -> fabCreate.show(), 150);
}
oldVisibility = currentVisibility;
@ -735,7 +741,6 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
searchView.setQuery(intent.getStringExtra(SearchManager.QUERY), true);
}
super.onNewIntent(intent);
Log.d(getClass().getSimpleName(), "onNewIntent: ");
}
/**
@ -749,14 +754,6 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (SingleSignOnAccount account) -> {
Log.v(getClass().getSimpleName(), "Added account: " + "name:" + account.name + ", " + account.url + ", userId" + account.userId);
db.addAccount(account.url, account.userId, account.name);
selectAccount(account.name);
clickHeader();
drawerLayout.closeDrawer(GravityCompat.START);
});
// Check which request we're responding to
if (requestCode == create_note_cmd) {
// Make sure the request was successful
@ -769,10 +766,10 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
if (createdNote != null) {
adapter.add(createdNote);
} else {
Log.w(NotesListViewActivity.class.getSimpleName(), "createdNote is null");
Log.w(TAG, "createdNote is null");
}
} else {
Log.w(NotesListViewActivity.class.getSimpleName(), "bundle is null");
Log.w(TAG, "bundle is null");
}
}
listView.scrollToPosition(0);
@ -780,6 +777,20 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
// Recreate activity completely, because theme switchting makes problems when only invalidating the views.
// @see https://github.com/stefan-niedermann/nextcloud-notes/issues/529
recreate();
} else {
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (SingleSignOnAccount account) -> {
Log.v(TAG, "Added account: " + "name:" + account.name + ", " + account.url + ", userId" + account.userId);
try {
db.addAccount(account.url, account.userId, account.name);
} catch(SQLiteConstraintException e) {
if(db.getAccounts().size() > 1) { // TODO ideally only show snackbar when this is a not migrated account
Snackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG).show();
}
}
selectAccount(account.name);
clickHeader();
drawerLayout.closeDrawer(GravityCompat.START);
});
}
}
@ -787,13 +798,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
try {
String url = localAccount.getUrl();
if (url != null) {
String croppedUrl = url;
try {
croppedUrl = new URL(url).getHost();
} catch (MalformedURLException e) {
e.printStackTrace();
}
this.account.setText(localAccount.getUserName() + "@" + croppedUrl);
this.account.setText(localAccount.getAccountName());
Glide
.with(this)
.load(url + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64")
@ -801,16 +806,16 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
.apply(RequestOptions.circleCropTransform())
.into(this.currentAccountImage);
} else {
Log.w(NotesListViewActivity.class.getSimpleName(), "url is null");
Log.w(TAG, "url is null");
}
} catch (NullPointerException e) {
} catch (NullPointerException e) { // No local account - show generic header
this.account.setText(R.string.app_name_long);
Glide
.with(this)
.load(R.mipmap.ic_launcher)
.apply(RequestOptions.circleCropTransform())
.into(this.currentAccountImage);
Log.w(getClass().getSimpleName(), "Tried to update username in drawer, but localAccount was null");
Log.w(TAG, "Tried to update username in drawer, but localAccount was null");
}
}
@ -824,7 +829,7 @@ public class NotesListViewActivity extends AppCompatActivity implements ItemAdap
v.setSelected(true);
}
int size = adapter.getSelected().size();
mActionMode.setTitle(String.valueOf(getResources().getQuantityString(R.plurals.ab_selected, size, size)));
mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, size, size));
int checkedItemCount = adapter.getSelected().size();
boolean hasCheckedItems = checkedItemCount > 0;

View file

@ -62,6 +62,7 @@ public class SelectSingleNoteActivity extends NotesListViewActivity {
finish();
}
assert extras != null;
int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
SharedPreferences.Editor sp = PreferenceManager.getDefaultSharedPreferences(this).edit();

View file

@ -24,6 +24,7 @@ import it.niedermann.owncloud.notes.model.LocalAccount;
import static it.niedermann.owncloud.notes.util.SSOUtil.askForNewAccount;
public class NoteListWidget extends AppWidgetProvider {
private static final String TAG = NoteListWidget.class.getSimpleName();
public static final String WIDGET_MODE_KEY = "NLW_mode";
public static final String WIDGET_CATEGORY_KEY = "NLW_cat";
public static final String WIDGET_ACCOUNT_KEY = "NLW_account";
@ -120,14 +121,14 @@ public class NoteListWidget extends AppWidgetProvider {
if (intent.getExtras() != null) {
updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)});
} else {
Log.w(NoteListWidget.class.getSimpleName(), "intent.getExtras() is null");
Log.w(TAG, "intent.getExtras() is null");
}
} else {
updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class)));
}
}
} else {
Log.w(NoteListWidget.class.getSimpleName(), "intent.getAction() is null");
Log.w(TAG, "intent.getAction() is null");
}
}

View file

@ -16,7 +16,6 @@ import it.niedermann.owncloud.notes.android.activity.EditNoteActivity;
import it.niedermann.owncloud.notes.android.fragment.BaseNoteFragment;
public class SingleNoteWidget extends AppWidgetProvider {
private static boolean darkTheme;
public static final String DARK_THEME_KEY = "SNW_darkTheme";
public static final String WIDGET_KEY = "single_note_widget";
@ -33,8 +32,8 @@ public class SingleNoteWidget extends AppWidgetProvider {
return;
}
boolean darkTheme = sp.getBoolean(DARK_THEME_KEY + appWidgetId, false);
templateIntent.putExtra(BaseNoteFragment.PARAM_ACCOUNT_ID, sp.getLong(ACCOUNT_ID_KEY + appWidgetId, -1));
darkTheme = sp.getBoolean(DARK_THEME_KEY + appWidgetId, false);
PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

View file

@ -46,6 +46,8 @@ import static it.niedermann.owncloud.notes.android.activity.EditNoteActivity.ACT
public abstract class BaseNoteFragment extends Fragment implements CategoryDialogFragment.CategoryDialogListener {
private static final String TAG = BaseNoteFragment.class.getSimpleName();
public interface NoteFragmentListener {
void close();
@ -94,38 +96,40 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
try {
this.localAccount = db.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getActivity().getApplicationContext()).name);
if (savedInstanceState == null) {
isNew = true;
long id = getArguments().getLong(PARAM_NOTE_ID);
if (id > 0) {
long accountId = getArguments().getLong(PARAM_ACCOUNT_ID);
if(accountId > 0) {
/* Switch account if account id has been provided */
this.localAccount = db.getAccount(accountId);
SingleAccountHelper.setCurrentAccount(getActivity().getApplicationContext(), localAccount.getAccountName());
try {
db.getNoteServerSyncHelper().updateAccount();
} catch(NextcloudFilesAppAccountNotFoundException e) {
e.printStackTrace();
}
}
note = originalNote = db.getNote(localAccount.getId(), id);
} else {
CloudNote cloudNote = (CloudNote) getArguments().getSerializable(PARAM_NEWNOTE);
if (cloudNote == null) {
throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given and argument " + PARAM_NEWNOTE + " is missing.");
}
note = db.getNote(localAccount.getId(), db.addNoteAndSync(localAccount.getId(), cloudNote));
originalNote = null;
}
} else {
isNew = false;
note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
}
setHasOptionsMenu(true);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
if (savedInstanceState == null) {
isNew = true;
long id = getArguments().getLong(PARAM_NOTE_ID);
if (id > 0) {
long accountId = getArguments().getLong(PARAM_ACCOUNT_ID);
Log.d(getClass().getSimpleName(), "onCreate: accountId: " + accountId);
if(accountId > 0) {
Log.d(getClass().getSimpleName(), "onCreate: switching from " + this.localAccount + " to " + accountId);
/* Switch account if account id has been provided */
this.localAccount = db.getAccount(accountId);
SingleAccountHelper.setCurrentAccount(getActivity().getApplicationContext(), localAccount.getAccountName());
db.getNoteServerSyncHelper().updateAccount();
}
note = originalNote = db.getNote(localAccount.getId(), id);
} else {
CloudNote cloudNote = (CloudNote) getArguments().getSerializable(PARAM_NEWNOTE);
if (cloudNote == null) {
throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given and argument " + PARAM_NEWNOTE + " is missing.");
}
note = db.getNote(localAccount.getId(), db.addNoteAndSync(localAccount.getId(), cloudNote));
originalNote = null;
}
} else {
isNew = false;
note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
}
setHasOptionsMenu(true);
}
@Override
@ -333,13 +337,17 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
* @param callback Observer which is called after save/synchronization
*/
protected void saveNote(@Nullable ICallback callback) {
Log.d(getClass().getSimpleName(), "saveData()");
String newContent = getContent();
if (note.getContent().equals(newContent)) {
Log.v(getClass().getSimpleName(), "... not saving, since nothing has changed");
Log.d(TAG, "saveData()");
if(note != null) {
String newContent = getContent();
if (note.getContent().equals(newContent)) {
Log.v(TAG, "... not saving, since nothing has changed");
} else {
note = db.updateNoteAndSync(localAccount.getId(), note, newContent, callback);
listener.onNoteUpdated(note);
}
} else {
note = db.updateNoteAndSync(localAccount.getId(), note, newContent, callback);
listener.onNoteUpdated(note);
Log.e(TAG, "note is null");
}
}
@ -349,6 +357,7 @@ public abstract class BaseNoteFragment extends Fragment implements CategoryDialo
final String prefValueLarge = getString(R.string.pref_value_font_size_large);
String fontSize = sp.getString(getString(R.string.pref_key_font_size), prefValueMedium);
assert fontSize != null;
if (fontSize.equals(prefValueSmall)) {
return getResources().getDimension(R.dimen.note_font_size_small);
} else if (fontSize.equals(prefValueMedium)) {

View file

@ -32,6 +32,8 @@ import it.niedermann.owncloud.notes.persistence.NoteSQLiteOpenHelper;
*/
public class CategoryDialogFragment extends DialogFragment {
private static final String TAG = CategoryDialogFragment.class.getSimpleName();
/**
* Interface that must be implemented by the calling Activity.
*/
@ -91,7 +93,7 @@ public class CategoryDialogFragment extends DialogFragment {
if (getDialog().getWindow() != null) {
getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
} else {
Log.w(CategoryDialogFragment.class.getSimpleName(), "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null");
Log.w(TAG, "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null");
}
}

View file

@ -25,6 +25,8 @@ import com.yydcdut.markdown.syntax.edit.EditFactory;
import com.yydcdut.rxmarkdown.RxMDEditText;
import com.yydcdut.rxmarkdown.RxMarkdown;
import java.util.Objects;
import butterknife.BindView;
import butterknife.ButterKnife;
import it.niedermann.owncloud.notes.R;
@ -114,9 +116,9 @@ public class NoteEditFragment extends BaseNoteFragment {
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(getView() != null) {
ButterKnife.bind(this, getView());
ButterKnife.bind(this, Objects.requireNonNull(getView()));
if(note != null) {
setActiveTextView(editContent);
if (note.getContent().isEmpty()) {
@ -161,8 +163,6 @@ public class NoteEditFragment extends BaseNoteFragment {
if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
editContent.setTypeface(Typeface.MONOSPACE);
}
} else {
Log.e(NoteEditFragment.class.getSimpleName(), "getView() is null");
}
}

View file

@ -19,6 +19,8 @@ import com.yydcdut.markdown.syntax.text.TextFactory;
import com.yydcdut.rxmarkdown.RxMDTextView;
import com.yydcdut.rxmarkdown.RxMarkdown;
import java.util.Objects;
import butterknife.BindView;
import butterknife.ButterKnife;
import it.niedermann.owncloud.notes.R;
@ -29,6 +31,8 @@ import rx.schedulers.Schedulers;
public class NotePreviewFragment extends BaseNoteFragment {
private static final String TAG = NotePreviewFragment.class.getSimpleName();
@BindView(R.id.single_note_content)
RxMDTextView noteContent;
@ -57,7 +61,7 @@ public class NotePreviewFragment extends BaseNoteFragment {
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ButterKnife.bind(this, getView());
ButterKnife.bind(this, Objects.requireNonNull(getView()));
setActiveTextView(noteContent);
@ -91,7 +95,7 @@ public class NotePreviewFragment extends BaseNoteFragment {
@Override
public void onError(Throwable e) {
Log.v(getClass().getSimpleName(), "RxMarkdown error", e);
Log.v(TAG, "RxMarkdown error", e);
}
@Override

View file

@ -14,6 +14,9 @@ import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.util.Notes;
public class PreferencesFragment extends PreferenceFragment {
private static final String TAG = PreferencesFragment.class.getSimpleName();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -37,7 +40,7 @@ public class PreferencesFragment extends PreferenceFragment {
final SwitchPreference wifiOnlyPref = (SwitchPreference) findPreference(getString(R.string.pref_key_wifi_only));
wifiOnlyPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
Boolean syncOnWifiOnly = (Boolean) newValue;
Log.v(getClass().getSimpleName(), "syncOnWifiOnly: " + syncOnWifiOnly);
Log.v(TAG, "syncOnWifiOnly: " + syncOnWifiOnly);
return true;
});
}

View file

@ -8,6 +8,7 @@ import android.content.pm.ShortcutManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Build;
@ -19,6 +20,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
@ -34,6 +37,7 @@ import it.niedermann.owncloud.notes.model.DBNote;
import it.niedermann.owncloud.notes.model.DBStatus;
import it.niedermann.owncloud.notes.model.LocalAccount;
import it.niedermann.owncloud.notes.model.NavigationAdapter;
import it.niedermann.owncloud.notes.util.DatabaseIndexUtil;
import it.niedermann.owncloud.notes.util.ICallback;
import it.niedermann.owncloud.notes.util.NoteUtil;
@ -42,6 +46,8 @@ import it.niedermann.owncloud.notes.util.NoteUtil;
*/
public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
private static final String TAG = NoteSQLiteOpenHelper.class.getSimpleName();
private static final int database_version = 9;
private static final String database_name = "OWNCLOUD_NOTES";
private static final String table_notes = "NOTES";
@ -72,13 +78,13 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
private NoteSQLiteOpenHelper(Context context) {
super(context, database_name, null, database_version);
this.context = context.getApplicationContext();
this.context = context;
serverSyncHelper = NoteServerSyncHelper.getInstance(this);
}
public static NoteSQLiteOpenHelper getInstance(Context context) {
if (instance == null)
return instance = new NoteSQLiteOpenHelper(context.getApplicationContext());
return instance = new NoteSQLiteOpenHelper(context);
else
return instance;
}
@ -94,8 +100,8 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNotesTable(db, table_notes);
createAccountTable(db, table_accounts);
createNotesTable(db, table_notes);
}
private void createNotesTable(@NonNull SQLiteDatabase db, @NonNull String tableName) {
@ -109,7 +115,8 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
key_content + " TEXT, " +
key_favorite + " INTEGER DEFAULT 0, " +
key_category + " TEXT NOT NULL DEFAULT '', " +
key_etag + " TEXT)");
key_etag + " TEXT," +
"FOREIGN KEY(" + key_account_id + ") REFERENCES " + table_accounts + "(" + key_id + "))");
createNotesIndexes(db);
}
@ -124,14 +131,14 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
createAccountIndexes(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 3) {
recreateDatabase(db);
}
if (oldVersion < 4) {
clearDatabase(db);
db.delete(table_notes, null, null);
db.delete(table_accounts, null, null);
}
if (oldVersion < 5) {
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_remote_id + " INTEGER");
@ -142,7 +149,7 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_favorite + " INTEGER DEFAULT 0");
}
if (oldVersion < 7) {
dropIndexes(db);
DatabaseIndexUtil.dropIndexes(db);
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_category + " TEXT NOT NULL DEFAULT ''");
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_etag + " TEXT");
createNotesIndexes(db);
@ -156,17 +163,59 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
db.execSQL(String.format("ALTER TABLE %s RENAME TO %s", table_temp, table_notes));
}
if (oldVersion < 9) {
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_account_id + " INTEGER NOT NULL DEFAULT 0");
createIndex(db, table_notes, key_account_id);
// Create accounts table
createAccountTable(db, table_accounts);
ContentValues values = new ContentValues();
values.put(key_account_id, 1);
db.update(table_notes, values, key_account_id + " = ?", new String[]{"NULL"});
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.remove("notes_last_etag");
editor.remove("notes_last_modified");
editor.apply();
// Add accountId to notes table
db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_account_id + " INTEGER NOT NULL DEFAULT 0");
DatabaseIndexUtil.createIndex(db, table_notes, key_account_id);
// Migrate existing account from SharedPreferences
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String username = sharedPreferences.getString("settingsUsername", "");
String url = sharedPreferences.getString("settingsUrl", "");
if (url != null && url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
try {
String accountName = username + "@" + new URL(url).getHost();
ContentValues migratedAccountValues = new ContentValues();
migratedAccountValues.put(key_url, url);
migratedAccountValues.put(key_username, username);
migratedAccountValues.put(key_account_name, accountName);
db.insert(table_accounts, null, migratedAccountValues);
// After successful insertion of migrated account, set accountId to 1 in each note
ContentValues values = new ContentValues();
values.put(key_account_id, 1);
db.update(table_notes, values, key_account_id + " = ?", new String[]{"NULL"});
// Add FOREIGN_KEY constraint
final String table_temp = "NOTES_TEMP";
db.execSQL(String.format("ALTER TABLE %s RENAME TO %s", table_notes, table_temp));
createNotesTable(db, table_notes);
db.execSQL(String.format("INSERT INTO %s(%s,%s, %s,%s,%s,%s,%s,%s,%s,%s) ", table_notes, key_id, key_account_id, key_remote_id, key_status, key_title, key_modified, key_content, key_favorite, key_category, key_etag)
+ String.format("SELECT %s,%s,%s, %s,%s,strftime('%%s',%s),%s,%s,%s,%s FROM %s", key_id, key_account_id, key_remote_id, key_status, key_title, key_modified, key_content, key_favorite, key_category, key_etag, table_temp));
db.execSQL(String.format("DROP TABLE %s;", table_temp));
// Clean up no longer needed SharedPreferences
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove("notes_last_etag");
editor.remove("notes_last_modified");
editor.remove("settingsUrl");
editor.remove("settingsUsername");
editor.remove("settingsPassword");
editor.apply();
} catch (MalformedURLException e) {
Log.e(TAG, "Previous URL could not be parsed. Recreating database...");
e.printStackTrace();
recreateDatabase(db);
}
} else {
Log.e(TAG, "Previous URL is null. Recreating database...");
recreateDatabase(db);
}
}
}
@ -175,67 +224,30 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
recreateDatabase(db);
}
private void clearDatabase(SQLiteDatabase db) {
db.delete(table_notes, null, null);
}
private void recreateDatabase(SQLiteDatabase db) {
dropIndexes(db);
db.execSQL("DROP TABLE " + table_notes);
DatabaseIndexUtil.dropIndexes(db);
db.execSQL("DROP TABLE IF EXISTS " + table_notes);
db.execSQL("DROP TABLE IF EXISTS " + table_accounts);
onCreate(db);
}
private void dropIndexes(SQLiteDatabase db) {
Cursor c = db.query("sqlite_master", new String[]{"name"}, "type=?", new String[]{"index"}, null, null, null);
while (c.moveToNext()) {
db.execSQL("DROP INDEX " + c.getString(0));
}
c.close();
private static void createNotesIndexes(@NonNull SQLiteDatabase db) {
DatabaseIndexUtil.createIndex(db, table_notes, key_remote_id, key_account_id, key_status, key_favorite, key_category, key_modified);
}
private void createNotesIndexes(SQLiteDatabase db) {
createIndex(db, table_notes, key_remote_id);
createIndex(db, table_notes, key_account_id);
createIndex(db, table_notes, key_status);
createIndex(db, table_notes, key_favorite);
createIndex(db, table_notes, key_category);
createIndex(db, table_notes, key_modified);
}
private void createAccountIndexes(SQLiteDatabase db) {
createIndex(db, table_accounts, key_url);
createIndex(db, table_accounts, key_username);
createIndex(db, table_accounts, key_account_name);
createIndex(db, table_accounts, key_etag);
createIndex(db, table_accounts, key_modified);
}
private void createIndex(SQLiteDatabase db, String table, String column) {
String indexName = table + "_" + column + "_idx";
db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")");
private static void createAccountIndexes(@NonNull SQLiteDatabase db) {
DatabaseIndexUtil.createIndex(db, table_accounts, key_url, key_username, key_account_name, key_etag, key_modified);
}
public Context getContext() {
return context;
}
/**
* Creates a new Note in the Database and adds a Synchronization Flag.
*
* @param content String
*/
@SuppressWarnings("UnusedReturnValue")
public long addNoteAndSync(String content, String category, boolean favorite, long accountId) {
CloudNote note = new CloudNote(0, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, getContext()), content, favorite, category, null);
return addNoteAndSync(accountId, note);
}
/**
* Creates a new Note in the Database and adds a Synchronization Flag.
*
* @param note Note
*/
@SuppressWarnings("UnusedReturnValue")
public long addNoteAndSync(long accountId, CloudNote note) {
DBNote dbNote = new DBNote(0, 0, note.getModified(), note.getTitle(), note.getContent(), note.isFavorite(), note.getCategory(), note.getEtag(), DBStatus.LOCAL_EDITED, accountId);
long id = addNote(accountId, dbNote);
@ -306,7 +318,7 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
private List<DBNote> getNotesCustom(long accountId, @NonNull String selection, @NonNull String[] selectionArgs, @Nullable String orderBy, @Nullable String limit) {
SQLiteDatabase db = getReadableDatabase();
if (selectionArgs.length > 2) {
Log.v(getClass().getSimpleName(), selection + " ---- " + selectionArgs[0] + " " + selectionArgs[1] + " " + selectionArgs[2]);
Log.v(TAG, selection + " ---- " + selectionArgs[0] + " " + selectionArgs[1] + " " + selectionArgs[2]);
}
Cursor cursor = db.query(table_notes, columns, selection, selectionArgs, null, null, orderBy, limit);
List<DBNote> notes = new ArrayList<>();
@ -325,22 +337,25 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
*/
@NonNull
private DBNote getNoteFromCursor(long accountId, @NonNull Cursor cursor) {
validateAccountId(accountId);
Calendar modified = Calendar.getInstance();
modified.setTimeInMillis(cursor.getLong(4) * 1000);
return new DBNote(cursor.getLong(0), cursor.getLong(1), modified, cursor.getString(3), cursor.getString(5), cursor.getInt(6) > 0, cursor.getString(7), cursor.getString(8), DBStatus.parse(cursor.getString(2)), accountId);
}
public void debugPrintFullDB(long accountId) {
validateAccountId(accountId);
List<DBNote> notes = getNotesCustom(accountId, "", new String[]{}, default_order);
Log.v(getClass().getSimpleName(), "Full Database (" + notes.size() + " notes):");
Log.v(TAG, "Full Database (" + notes.size() + " notes):");
for (DBNote note : notes) {
Log.v(getClass().getSimpleName(), " " + note);
Log.v(TAG, " " + note);
}
}
@NonNull
@WorkerThread
public Map<Long, Long> getIdMap(long accountId) {
validateAccountId(accountId);
Map<Long, Long> result = new HashMap<>();
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(table_notes, new String[]{key_remote_id, key_id}, key_status + " != ? AND " + key_account_id + " = ? ", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, null, null, null);
@ -359,12 +374,14 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
@NonNull
@WorkerThread
public List<DBNote> getNotes(long accountId) {
validateAccountId(accountId);
return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, default_order);
}
@NonNull
@WorkerThread
public List<DBNote> getRecentNotes(long accountId) {
validateAccountId(accountId);
return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.LOCAL_DELETED.getTitle(), "" + accountId}, key_modified + " DESC", "4");
}
@ -417,13 +434,15 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
*/
@NonNull
@WorkerThread
public List<DBNote> getLocalModifiedNotes(long accountId) {
List<DBNote> getLocalModifiedNotes(long accountId) {
validateAccountId(accountId);
return getNotesCustom(accountId, key_status + " != ? AND " + key_account_id + " = ?", new String[]{DBStatus.VOID.getTitle(), "" + accountId}, null);
}
@NonNull
@WorkerThread
public Map<String, Integer> getFavoritesCount(long accountId) {
validateAccountId(accountId);
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(
table_notes,
@ -444,6 +463,7 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
@NonNull
@WorkerThread
public List<NavigationAdapter.NavigationItem> getCategories(long accountId) {
validateAccountId(accountId);
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(
table_notes,
@ -548,9 +568,8 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
* @param id local ID of Note
* @param remoteNote Note from the server.
* @param forceUnchangedDBNoteState is not null, then the local note is updated only if it was not modified meanwhile
* @return The number of the Rows affected.
*/
int updateNote(long id, @NonNull CloudNote remoteNote, @Nullable DBNote forceUnchangedDBNoteState) {
void updateNote(long id, @NonNull CloudNote remoteNote, @Nullable DBNote forceUnchangedDBNoteState) {
SQLiteDatabase db = this.getWritableDatabase();
// First, update the remote ID, since this field cannot be changed in parallel, but have to be updated always.
@ -584,8 +603,7 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
whereArgs = new String[]{String.valueOf(id), DBStatus.VOID.getTitle(), Long.toString(remoteNote.getModified().getTimeInMillis() / 1000), remoteNote.getTitle(), remoteNote.isFavorite() ? "1" : "0", remoteNote.getCategory(), remoteNote.getEtag(), remoteNote.getContent()};
}
int i = db.update(table_notes, values, whereClause, whereArgs);
Log.d(getClass().getSimpleName(), "updateNote: " + remoteNote + " || forceUnchangedDBNoteState: " + forceUnchangedDBNoteState + " => " + i + " rows updated");
return i;
Log.d(TAG, "updateNote: " + remoteNote + " || forceUnchangedDBNoteState: " + forceUnchangedDBNoteState + " => " + i + " rows updated");
}
/**
@ -593,10 +611,8 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
* from the Server.
*
* @param id long - ID of the Note that should be deleted
* @return Affected rows
*/
@SuppressWarnings("UnusedReturnValue")
public int deleteNoteAndSync(long id) {
public void deleteNoteAndSync(long id) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(key_status, DBStatus.LOCAL_DELETED.getTitle());
@ -612,12 +628,11 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
shortcutManager.getPinnedShortcuts().forEach((shortcut) -> {
String shortcutId = id + "";
if (shortcut.getId().equals(shortcutId)) {
Log.v(NoteSQLiteOpenHelper.class.getSimpleName(), "Removing shortcut for " + shortcutId);
Log.v(TAG, "Removing shortcut for " + shortcutId);
shortcutManager.disableShortcuts(Collections.singletonList(shortcutId), context.getResources().getString(R.string.note_has_been_deleted));
}
});
}
return i;
}
/**
@ -638,44 +653,57 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
* Notify about changed notes.
*/
void notifyNotesChanged() {
updateSingleNoteWidgets();
updateNoteListWidgets();
updateSingleNoteWidgets(getContext());
updateNoteListWidgets(getContext());
}
/**
* Update single note widget, if the note data was changed.
*/
private void updateSingleNoteWidgets() {
Intent intent = new Intent(getContext(), SingleNoteWidget.class);
private static void updateSingleNoteWidgets(Context context) {
Intent intent = new Intent(context, SingleNoteWidget.class);
intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
getContext().sendBroadcast(intent);
context.sendBroadcast(intent);
}
/**
* Update note list widgets, if the note data was changed.
*/
private void updateNoteListWidgets() {
Intent intent = new Intent(getContext(), NoteListWidget.class);
private static void updateNoteListWidgets(Context context) {
Intent intent = new Intent(context, NoteListWidget.class);
intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
getContext().sendBroadcast(intent);
context.sendBroadcast(intent);
}
public boolean hasAccounts() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(), table_accounts) > 0;
}
public void addAccount(String url, String username, String accountName) {
/**
*
* @param url URL to the root of the used Nextcloud instance without trailing slash
* @param username Username of the account
* @param accountName Composed by the username and the host of the URL, separated by @-sign
* @throws SQLiteConstraintException in case accountName already exists
*/
public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName) throws SQLiteConstraintException {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(key_url, url);
values.put(key_username, username);
values.put(key_account_name, accountName);
db.insert(table_accounts, null, values);
db.insertOrThrow(table_accounts, null, values);
}
public LocalAccount getAccount(long i) {
/**
*
* @param accountId account which should be read
* @return a LocalAccount object for the given accountId
*/
public LocalAccount getAccount(long accountId) {
validateAccountId(accountId);
SQLiteDatabase db = getReadableDatabase();
Cursor cursor = db.query(table_accounts, new String[]{key_id, key_url, key_account_name, key_username, key_etag, key_modified}, key_id + " = ?", new String[]{i + ""}, null, null, null, null);
Cursor cursor = db.query(table_accounts, new String[]{key_id, key_url, key_account_name, key_username, key_etag, key_modified}, key_id + " = ?", new String[]{accountId + ""}, null, null, null, null);
LocalAccount account = new LocalAccount();
while (cursor.moveToNext()) {
account.setId(cursor.getLong(0));
@ -707,8 +735,10 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
return accounts;
}
@Nullable
public LocalAccount getLocalAccountByAccountName(String accountName) {
if (accountName == null) {
Log.e(TAG, "accountName is null");
return null;
}
SQLiteDatabase db = getReadableDatabase();
@ -726,39 +756,57 @@ public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
return account;
}
public void deleteAccount(long accountId) {
/**
*
* @param accountId the id of the account that should be deleted
* @throws IllegalArgumentException if no account has been deleted by the given accountId
*/
public void deleteAccount(long accountId) throws IllegalArgumentException {
validateAccountId(accountId);
SQLiteDatabase db = this.getWritableDatabase();
int deletedAccounts = db.delete(table_accounts, key_id + " = ?", new String[]{accountId + ""});
if (deletedAccounts < 1) {
Log.e(TAG, "AccountId '" + accountId + "' did not delete any account");
throw new IllegalArgumentException("The given accountId does not delete any row");
} else if (deletedAccounts > 1) {
Log.e(getClass().getSimpleName(), "AccountId '" + accountId + "' deleted unexpectedly '" + deletedAccounts + "' accounts");
Log.e(TAG, "AccountId '" + accountId + "' deleted unexpectedly '" + deletedAccounts + "' accounts");
}
final int deletedNotes = db.delete(table_notes, key_account_id + " = ?", new String[]{accountId + ""});
Log.v(getClass().getSimpleName(), "Deleted " + deletedNotes + " notes from account " + accountId);
Log.v(TAG, "Deleted " + deletedNotes + " notes from account " + accountId);
}
void updateETag(long accountId, String etag) {
validateAccountId(accountId);
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(key_etag, etag);
final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{accountId + ""});
if (updatedRows == 1) {
Log.v(getClass().getSimpleName(), "Updated etag to " + etag + " for accountId = " + accountId);
Log.v(TAG, "Updated etag to " + etag + " for accountId = " + accountId);
} else {
Log.e(getClass().getSimpleName(), "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and etag = " + etag);
Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and etag = " + etag);
}
}
void updateModified(long accountId, long modified) {
validateAccountId(accountId);
if(modified < 0) {
throw new IllegalArgumentException("modified must be greater or equal 0");
}
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(key_modified, modified);
final int updatedRows = db.update(table_accounts, values, key_id + " = ?", new String[]{accountId + ""});
if (updatedRows == 1) {
Log.v(getClass().getSimpleName(), "Updated modified to " + modified + " for accountId = " + accountId);
Log.v(TAG, "Updated modified to " + modified + " for accountId = " + accountId);
} else {
Log.e(getClass().getSimpleName(), "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and modified = " + modified);
Log.e(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and modified = " + modified);
}
}
private static void validateAccountId(long accountId) {
if(accountId < 1) {
throw new IllegalArgumentException("accountId must be greater than 0");
}
}
}

View file

@ -39,6 +39,8 @@ import it.niedermann.owncloud.notes.util.ServerResponse;
*/
public class NoteServerSyncHelper {
private static final String TAG = NoteServerSyncHelper.class.getSimpleName();
private static NoteServerSyncHelper instance;
/**
@ -99,10 +101,8 @@ public class NoteServerSyncHelper {
this.dbHelper = db;
this.appContext = db.getContext().getApplicationContext();
try {
this.localAccount = db.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(appContext).name);
notesClient = new NotesClient(appContext);
Log.v(getClass().getSimpleName(), "NextcloudRequest account: " + localAccount);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
updateAccount();
} catch (NextcloudFilesAppAccountNotFoundException e) {
e.printStackTrace();
}
this.syncOnlyOnWifiKey = appContext.getResources().getString(R.string.pref_key_wifi_only);
@ -117,7 +117,7 @@ public class NoteServerSyncHelper {
updateNetworkStatus();
}
public void updateAccount() {
public void updateAccount() throws NextcloudFilesAppAccountNotFoundException {
try {
this.localAccount = dbHelper.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(appContext).name);
if (notesClient == null) {
@ -127,11 +127,11 @@ public class NoteServerSyncHelper {
} else {
notesClient.updateAccount();
}
Log.v(getClass().getSimpleName(), "NextcloudRequest account: " + localAccount);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
Log.v(TAG, "NextcloudRequest account: " + localAccount);
} catch (NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
Log.v(getClass().getSimpleName(), "Reinstanziation NotesClient because of SSO acc changed");
Log.v(TAG, "Reinstanziation NotesClient because of SSO acc changed");
}
@Override
@ -195,9 +195,9 @@ public class NoteServerSyncHelper {
* @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
*/
public void scheduleSync(boolean onlyLocalChanges) {
Log.d(getClass().getSimpleName(), "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (syncActive ? "sync active" : "sync NOT active") + ") ...");
Log.d(TAG, "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (syncActive ? "sync active" : "sync NOT active") + ") ...");
if (isSyncPossible() && (!syncActive || onlyLocalChanges)) {
Log.d(getClass().getSimpleName(), "... starting now");
Log.d(TAG, "... starting now");
SyncTask syncTask = new SyncTask(onlyLocalChanges);
syncTask.addCallbacks(callbacksPush);
callbacksPush = new ArrayList<>();
@ -207,13 +207,13 @@ public class NoteServerSyncHelper {
}
syncTask.execute();
} else if (!onlyLocalChanges) {
Log.d(getClass().getSimpleName(), "... scheduled");
Log.d(TAG, "... scheduled");
syncScheduled = true;
for (ICallback callback : callbacksPush) {
callback.onScheduled();
}
} else {
Log.d(getClass().getSimpleName(), "... do nothing");
Log.d(TAG, "... do nothing");
for (ICallback callback : callbacksPush) {
callback.onScheduled();
}
@ -231,13 +231,13 @@ public class NoteServerSyncHelper {
.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected();
if (networkConnected) {
Log.d(NoteServerSyncHelper.class.getSimpleName(), "Network connection established.");
Log.d(TAG, "Network connection established.");
} else {
Log.d(NoteServerSyncHelper.class.getSimpleName(), "Network connected, but not used because only synced on wifi.");
Log.d(TAG, "Network connected, but not used because only synced on wifi.");
}
} else {
networkConnected = false;
Log.d(NoteServerSyncHelper.class.getSimpleName(), "No network connection.");
Log.d(TAG, "No network connection.");
}
}
@ -269,7 +269,7 @@ public class NoteServerSyncHelper {
@Override
protected LoginStatus doInBackground(Void... voids) {
Log.i(getClass().getSimpleName(), "STARTING SYNCHRONIZATION");
Log.i(TAG, "STARTING SYNCHRONIZATION");
//dbHelper.debugPrintFullDB();
LoginStatus status = LoginStatus.OK;
pushLocalChanges();
@ -277,7 +277,7 @@ public class NoteServerSyncHelper {
status = pullRemoteChanges();
}
//dbHelper.debugPrintFullDB();
Log.i(getClass().getSimpleName(), "SYNCHRONIZATION FINISHED");
Log.i(TAG, "SYNCHRONIZATION FINISHED");
return status;
}
@ -288,34 +288,34 @@ public class NoteServerSyncHelper {
if (localAccount == null) {
return;
}
Log.d(getClass().getSimpleName(), "pushLocalChanges()");
Log.d(TAG, "pushLocalChanges()");
List<DBNote> notes = dbHelper.getLocalModifiedNotes(localAccount.getId());
for (DBNote note : notes) {
Log.d(getClass().getSimpleName(), " Process Local Note: " + note);
Log.d(TAG, " Process Local Note: " + note);
try {
CloudNote remoteNote = null;
switch (note.getStatus()) {
case LOCAL_EDITED:
Log.v(getClass().getSimpleName(), " ...create/edit");
Log.v(TAG, " ...create/edit");
// if note is not new, try to edit it.
if (note.getRemoteId() > 0) {
Log.v(getClass().getSimpleName(), " ...try to edit");
Log.v(TAG, " ...try to edit");
remoteNote = notesClient.editNote(note).getNote();
}
// However, the note may be deleted on the server meanwhile; or was never synchronized -> (re)create
// Please note, thas dbHelper.updateNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
if (remoteNote == null) {
Log.v(getClass().getSimpleName(), " ...Note does not exist on server -> (re)create");
Log.v(TAG, " ...Note does not exist on server -> (re)create");
remoteNote = notesClient.createNote(note).getNote();
}
dbHelper.updateNote(note.getId(), remoteNote, note);
break;
case LOCAL_DELETED:
if (note.getRemoteId() > 0) {
Log.v(getClass().getSimpleName(), " ...delete (from server and local)");
Log.v(TAG, " ...delete (from server and local)");
notesClient.deleteNote(note.getRemoteId());
} else {
Log.v(getClass().getSimpleName(), " ...delete (only local, since it was not synchronized)");
Log.v(TAG, " ...delete (only local, since it was not synchronized)");
}
// Please note, thas dbHelper.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
dbHelper.deleteNote(note.getId(), DBStatus.LOCAL_DELETED);
@ -324,7 +324,7 @@ public class NoteServerSyncHelper {
throw new IllegalStateException("Unknown State of Note: " + note);
}
} catch (JSONException e) {
Log.e(getClass().getSimpleName(), "Exception", e);
Log.e(TAG, "Exception", e);
exceptions.add(e);
}
}
@ -337,7 +337,7 @@ public class NoteServerSyncHelper {
if (localAccount == null) {
return LoginStatus.NO_NETWORK;
}
Log.d(getClass().getSimpleName(), "pullRemoteChanges() for account " + localAccount.getAccountName());
Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
LoginStatus status;
try {
Map<Long, Long> idMap = dbHelper.getIdMap(localAccount.getId());
@ -346,23 +346,23 @@ public class NoteServerSyncHelper {
Set<Long> remoteIDs = new HashSet<>();
// pull remote changes: update or create each remote note
for (CloudNote remoteNote : remoteNotes) {
Log.v(getClass().getSimpleName(), " Process Remote Note: " + remoteNote);
Log.v(TAG, " Process Remote Note: " + remoteNote);
remoteIDs.add(remoteNote.getRemoteId());
if (remoteNote.getModified() == null) {
Log.v(getClass().getSimpleName(), " ... unchanged");
Log.v(TAG, " ... unchanged");
} else if (idMap.containsKey(remoteNote.getRemoteId())) {
Log.v(getClass().getSimpleName(), " ... found -> Update");
Log.v(TAG, " ... found -> Update");
dbHelper.updateNote(idMap.get(remoteNote.getRemoteId()), remoteNote, null);
} else {
Log.v(getClass().getSimpleName(), " ... create");
Log.v(TAG, " ... create");
dbHelper.addNote(localAccount.getId(), remoteNote);
}
}
Log.d(getClass().getSimpleName(), " Remove remotely deleted Notes (only those without local changes)");
Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
// remove remotely deleted notes (only those without local changes)
for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
if (!remoteIDs.contains(entry.getKey())) {
Log.v(getClass().getSimpleName(), " ... remove " + entry.getValue());
Log.v(TAG, " ... remove " + entry.getValue());
dbHelper.deleteNote(entry.getValue(), DBStatus.VOID);
}
}
@ -374,7 +374,7 @@ public class NoteServerSyncHelper {
dbHelper.updateModified(localAccount.getId(), localAccount.getModified());
return LoginStatus.OK;
} catch (JSONException e) {
Log.e(getClass().getSimpleName(), "Exception", e);
Log.e(TAG, "Exception", e);
exceptions.add(e);
status = LoginStatus.JSON_FAILED;
}

View file

@ -0,0 +1,32 @@
package it.niedermann.owncloud.notes.util;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.NonNull;
public class DatabaseIndexUtil {
private DatabaseIndexUtil() {
}
public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String ...columns) {
for (String column: columns) {
createIndex(db, table, column);
}
}
public static void createIndex(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) {
String indexName = table + "_" + column + "_idx";
db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")");
}
public static void dropIndexes(@NonNull SQLiteDatabase db) {
Cursor c = db.query("sqlite_master", new String[]{"name"}, "type=?", new String[]{"index"}, null, null, null);
while (c.moveToNext()) {
db.execSQL("DROP INDEX " + c.getString(0));
}
c.close();
}
}

View file

@ -26,16 +26,16 @@ import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import androidx.annotation.ColorInt;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.ColorInt;
public class DisplayUtils {
public static Spannable searchAndColor(String text, Spannable spannable, String searchText, @ColorInt int color) {
Object spansToRemove[] = spannable.getSpans(0, text.length(), Object.class);
Object[] spansToRemove = spannable.getSpans(0, text.length(), Object.class);
for(Object span: spansToRemove){
if(span instanceof CharacterStyle)
spannable.removeSpan(span);

View file

@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import it.niedermann.owncloud.notes.model.CloudNote;
import it.niedermann.owncloud.notes.util.ServerResponse.NoteResponse;
@ -31,6 +32,8 @@ import it.niedermann.owncloud.notes.util.ServerResponse.NotesResponse;
@WorkerThread
public class NotesClient {
private static final String TAG = NotesClient.class.getSimpleName();
private final Context context;
private NextcloudAPI mNextcloudAPI;
@ -61,6 +64,8 @@ public class NotesClient {
}
}
private static final String HEADER_ETAG = "ETag";
private static final String HEADER_LAST_MODIFIED = "Last-Modified";
public static final String METHOD_GET = "GET";
public static final String METHOD_PUT = "PUT";
public static final String METHOD_POST = "POST";
@ -76,23 +81,7 @@ public class NotesClient {
public NotesClient(Context context) {
this.context = context;
try {
SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
Log.v(getClass().getSimpleName(), "NextcloudRequest account: " + ssoAccount.name);
mNextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create(), new NextcloudAPI.ApiConnectedListener() {
@Override
public void onConnected() {
Log.v(getClass().getSimpleName(), "SSO API connected");
}
@Override
public void onError(Exception ex) {
ex.printStackTrace();
}
});
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
e.printStackTrace();
}
updateAccount();
}
public void updateAccount() {
@ -101,11 +90,11 @@ public class NotesClient {
}
try {
SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
Log.v(getClass().getSimpleName(), "NextcloudRequest account: " + ssoAccount.name);
Log.v(TAG, "NextcloudRequest account: " + ssoAccount.name);
mNextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create(), new NextcloudAPI.ApiConnectedListener() {
@Override
public void onConnected() {
Log.v(getClass().getSimpleName(), "SSO API connected");
Log.v(TAG, "SSO API connected");
}
@Override
@ -126,17 +115,6 @@ public class NotesClient {
return new NotesResponse(requestServer(url, METHOD_GET, null, lastETag));
}
/**
* Fetches a Note by ID from Server
*
* @param id long - ID of the wanted note
* @return Requested Note
*/
@SuppressWarnings("unused")
public NoteResponse getNoteById(long id) {
return new NoteResponse(requestServer("notes/" + id, METHOD_GET, null, null));
}
private NoteResponse putNote(CloudNote note, String path, String method) throws JSONException {
JSONObject paramObject = new JSONObject();
paramObject.accumulate(JSON_CONTENT, note.getContent());
@ -146,7 +124,6 @@ public class NotesClient {
return new NoteResponse(requestServer(path, method, paramObject, null));
}
/**
* Creates a Note on the Server
*
@ -194,9 +171,9 @@ public class NotesClient {
StringBuilder result = new StringBuilder();
try {
Log.v(getClass().getSimpleName(), "NextcloudRequest: " + nextcloudRequest.toString());
Log.v(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
InputStream inputStream = mNextcloudAPI.performNetworkRequest(nextcloudRequest);
Log.v(getClass().getSimpleName(), "NextcloudRequest: " + nextcloudRequest.toString());
Log.v(TAG, "NextcloudRequest: " + nextcloudRequest.toString());
BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = rd.readLine()) != null) {
@ -207,13 +184,13 @@ public class NotesClient {
e.printStackTrace();
}
String etag = "";
if (nextcloudRequest.getHeader().get("ETag") != null) {
etag = nextcloudRequest.getHeader().get("ETag").get(0);
if (nextcloudRequest.getHeader().get(HEADER_ETAG) != null) {
etag = Objects.requireNonNull(nextcloudRequest.getHeader().get(HEADER_ETAG)).get(0);
}
long lastModified = 0;
if (nextcloudRequest.getHeader().get("Last-Modified") != null)
lastModified = Long.parseLong(nextcloudRequest.getHeader().get("Last-Modified").get(0)) / 1000;
Log.d(getClass().getSimpleName(), "ETag: " + etag + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
if (nextcloudRequest.getHeader().get(HEADER_LAST_MODIFIED) != null)
lastModified = Long.parseLong(Objects.requireNonNull(nextcloudRequest.getHeader().get(HEADER_LAST_MODIFIED)).get(0)) / 1000;
Log.d(TAG, "ETag: " + etag + "; Last-Modified: " + lastModified + " (" + lastModified + ")");
// return these header fields since they should only be saved after successful processing the result!
return new ResponseData(result.toString(), etag, lastModified);
}

View file

@ -12,7 +12,6 @@ public class NotesClientUtil {
public enum LoginStatus {
OK(0),
CONNECTION_FAILED(R.string.error_io),
NO_NETWORK(R.string.error_no_network),
JSON_FAILED(R.string.error_json);

View file

@ -1,31 +1,78 @@
package it.niedermann.owncloud.notes.util;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.Constants;
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
import com.nextcloud.android.sso.ui.UiExceptionManager;
import it.niedermann.owncloud.notes.android.activity.NotesListViewActivity;
import static com.nextcloud.android.sso.AccountImporter.CHOOSE_ACCOUNT_SSO;
public class SSOUtil {
private static final String TAG = SSOUtil.class.getSimpleName();
private SSOUtil() {
}
public static void askForNewAccount(Activity activity) {
/**
* Opens a dialog which allows the user to pick a Nextcloud account (which previously has to be configured in the files app).
* Also allows to configure a new Nextcloud account in the files app and directly import it.
*
* @param activity should implement AccountImporter.onActivityResult
*/
public static void askForNewAccount(@NonNull Activity activity) {
try {
AccountImporter.pickNewAccount(activity);
} catch (NextcloudFilesAppNotInstalledException e1) {
UiExceptionManager.showDialogForException(activity, e1);
Log.w(NotesListViewActivity.class.toString(), "=============================================================");
Log.w(NotesListViewActivity.class.toString(), "Nextcloud app is not installed. Cannot choose account");
Log.w(SSOUtil.class.toString(), "=============================================================");
Log.w(SSOUtil.class.toString(), "Nextcloud app is not installed. Cannot choose account");
e1.printStackTrace();
} catch (AndroidGetAccountsPermissionNotGranted e2) {
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(activity);
}
}
/**
* Opens the same dialog like AccountImporter.pickNewAccount() but preselects the given account
*
* @param activity should implement CHOOSE_ACCOUNT_SSO in onActivityResult
* @param accountName account that should be preselected
*/
public static void authorizeExistingAccount(@NonNull Activity activity, @NonNull String accountName) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.GET_ACCOUNTS) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Permission not granted.");
// Well... do you want to use this SSO account or not?
return;
}
}
Log.d(TAG, "Permission granted.");
Intent intent = AccountManager.newChooseAccountIntent(
new Account(accountName, Constants.ACCOUNT_TYPE_PROD),
null,
new String[]{Constants.ACCOUNT_TYPE_PROD, Constants.ACCOUNT_TYPE_DEV},
true,
null,
null,
null,
null
);
activity.startActivityForResult(intent, CHOOSE_ACCOUNT_SSO);
}
}

View file

@ -6,26 +6,16 @@ import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.annotation.NonNull;
/**
* Some helper functionality in alike the Android support library.
* Currently, it offers methods for working with HTML string resources.
*/
public class SupportUtil {
/**
* Creates a {@link Spanned} from a HTML string on all SDK versions.
*
* @param source Source string with HTML markup
* @return Spannable for using in a {@link TextView}
* @see Html#fromHtml(String)
* @see Html#fromHtml(String, int)
*/
public static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= 24) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
return Html.fromHtml(source);
}
private SupportUtil() {
}
/**
@ -35,8 +25,24 @@ public class SupportUtil {
* @param stringId The string resource containing HTML tags (escaped by <code>&lt;</code>)
* @param formatArgs Arguments for the string resource.
*/
public static void setHtml(TextView view, int stringId, Object... formatArgs) {
public static void setHtml(@NonNull TextView view, int stringId, Object... formatArgs) {
view.setText(SupportUtil.fromHtml(view.getResources().getString(stringId, formatArgs)));
view.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* Creates a {@link Spanned} from a HTML string on all SDK versions.
*
* @param source Source string with HTML markup
* @return Spannable for using in a {@link TextView}
* @see Html#fromHtml(String)
* @see Html#fromHtml(String, int)
*/
private static Spanned fromHtml(String source) {
if (Build.VERSION.SDK_INT >= 24) {
return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
} else {
return Html.fromHtml(source);
}
}
}

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

View file

@ -146,6 +146,7 @@
<string name="sso_announcment_message">The Notes app will beginning with the next major version use the great Single-Sign-On-Feature of Nextcloud. This will increase the security, reliability and comfort for you. Please make sure, you have installed at least version 3.8.0 of the files app and select at the first run the same account which you are already using.</string>
<string name="sso_announcment_more_info">More information</string>
<string name="sso_announcment_understood">Understood</string>
<string name="account_already_imported">Account has already been imported</string>
<!-- Array: note modes -->
<string-array name="noteMode_entries">