diff options
author | Stefan Niedermann <info@niedermann.it> | 2021-06-28 11:06:50 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2021-06-28 11:06:50 +0300 |
commit | 9fe1b079efa58a822c3432859abbc9565739543b (patch) | |
tree | 7dd87a5a9b838832374084d20770d20b2d4ff3d5 /app/src/main/java/it/niedermann/owncloud | |
parent | 0d1136aad3cae384e7a9c636f79e4d121b28dd89 (diff) | |
parent | 7c5cb1b2552ee711eeaec98e3e8928e8e033cb0a (diff) |
Merge branch 'master' into 916-settings
# Conflicts:
# app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
# app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java
# app/src/main/res/values/strings.xml
Diffstat (limited to 'app/src/main/java/it/niedermann/owncloud')
56 files changed, 959 insertions, 567 deletions
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java index fc4b8fc6..29d2d0ad 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java @@ -42,7 +42,7 @@ public class AppendToNoteActivity extends MainActivity { fullNote$.removeObservers(this); final String oldContent = fullNote.getContent(); String newContent; - if (oldContent != null && oldContent.length() > 0) { + if (!TextUtils.isEmpty(oldContent)) { newContent = oldContent + "\n\n" + receivedText; } else { newContent = receivedText; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index f5643d86..e1ba3e8e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -67,7 +67,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { account$.observe(requireActivity(), (currentLocalAccount) -> { account$.removeObservers(requireActivity()); - binding.accountName.setText(currentLocalAccount.getUserName()); + binding.accountName.setText(currentLocalAccount.getDisplayName()); binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost()); Glide.with(requireContext()) .load(currentLocalAccount.getUrl() + "/index.php/avatar/" + Uri.encode(currentLocalAccount.getUserName()) + "/64") diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java index db04e0db..1f096c96 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java @@ -25,7 +25,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { } public void bind(@NonNull Account localAccount, @NonNull Consumer<Account> onAccountClick) { - binding.accountName.setText(localAccount.getUserName()); + binding.accountName.setText(localAccount.getDisplayName()); binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost()); Glide.with(itemView.getContext()) .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64")) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index b1cf1d54..4fc883ac 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -24,6 +24,7 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.LiveData; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; @@ -32,6 +33,8 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.util.ArrayList; import java.util.Calendar; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import it.niedermann.android.util.ColorUtil; import it.niedermann.owncloud.notes.R; @@ -47,6 +50,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.ApiVersion; import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.NotesColorUtil; import it.niedermann.owncloud.notes.shared.util.ShareUtil; @@ -60,6 +64,7 @@ import static java.lang.Boolean.TRUE; public abstract class BaseNoteFragment extends BrandedFragment implements CategoryDialogListener, EditTitleListener { private static final String TAG = BaseNoteFragment.class.getSimpleName(); + protected final ExecutorService executor = Executors.newCachedThreadPool(); protected static final int MENU_ID_PIN = -1; public static final String PARAM_NOTE_ID = "noteId"; @@ -94,9 +99,9 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - new Thread(() -> { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + executor.submit(() -> { try { SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext()); this.localAccount = repo.getAccountByName(ssoAccount.name); @@ -141,7 +146,7 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { e.printStackTrace(); } - }).start(); + }); setHasOptionsMenu(true); } @@ -193,7 +198,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego if (note != null) { prepareFavoriteOption(menu.findItem(R.id.menu_favorite)); - menu.findItem(R.id.menu_title).setVisible(localAccount.getPreferredApiVersion() != null && localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) >= 0); + final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion()); + menu.findItem(R.id.menu_title).setVisible(preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0); menu.findItem(R.id.menu_delete).setVisible(!isNew); } } @@ -211,13 +217,13 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.menu_cancel) { - new Thread(() -> { + executor.submit(() -> { if (originalNote == null) { repo.deleteNoteAndSync(localAccount, note.getId()); } else { repo.updateNoteAndSync(localAccount, originalNote, null, null, null); } - }).start(); + }); listener.close(); return true; } else if (itemId == R.id.menu_delete) { @@ -236,11 +242,9 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego showEditTitleDialog(); return true; } else if (itemId == R.id.menu_move) { - new Thread(() -> { - AccountPickerDialogFragment - .newInstance(new ArrayList<>(), note.getAccountId()) - .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName()); - }).start(); + executor.submit(() -> AccountPickerDialogFragment + .newInstance(new ArrayList<>(repo.getAccounts()), note.getAccountId()) + .show(requireActivity().getSupportFragmentManager(), BaseNoteFragment.class.getSimpleName())); return true; } else if (itemId == R.id.menu_share) { ShareUtil.openShareDialog(requireContext(), note.getTitle(), note.getContent()); @@ -363,14 +367,15 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego public void onTitleEdited(String newTitle) { titleModified = true; note.setTitle(newTitle); - new Thread(() -> { + executor.submit(() -> { note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null); requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note)); - }).start(); + }); } public void moveNote(Account account) { - repo.moveNoteToAnotherAccount(account, note); + final LiveData<Note> moveLiveData = repo.moveNoteToAnotherAccount(account, note); + moveLiveData.observe(this, (v) -> moveLiveData.removeObservers(this)); listener.close(); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java index 298a6c3f..83c8eb1a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java @@ -29,6 +29,7 @@ import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; +import it.niedermann.owncloud.notes.shared.util.DisplayUtils; import static androidx.core.view.ViewCompat.isAttachedToWindow; import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences; @@ -59,6 +60,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } }; private TextWatcher textWatcher; + private boolean keyboardShown = false; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -138,22 +140,17 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { public void onResume() { super.onResume(); binding.editContent.addTextChangedListener(textWatcher); + + if (keyboardShown) { + openSoftKeyboard(); + } } @Override protected void onNoteLoaded(Note note) { super.onNoteLoaded(note); if (TextUtils.isEmpty(note.getContent())) { - binding.editContent.post(() -> { - binding.editContent.requestFocus(); - - final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); - } else { - Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); - } - }); + openSoftKeyboard(); } binding.editContent.setMarkdownString(note.getContent()); @@ -166,11 +163,32 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { } } + private void openSoftKeyboard() { + binding.editContent.postDelayed(() -> { + binding.editContent.requestFocus(); + + final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT); + } else { + Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null."); + } + //Without a small delay the keyboard does not show reliably + }, 100); + } + @Override public void onPause() { super.onPause(); binding.editContent.removeTextChangedListener(textWatcher); cancelTimers(); + + final ViewGroup parentView = requireActivity().findViewById(android.R.id.content); + if (parentView != null && parentView.getChildCount() > 0) { + keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView.getChildAt(0)); + } else { + keyboardShown = false; + } } private void cancelTimers() { @@ -184,7 +202,8 @@ public class NoteEditFragment extends SearchableBaseNoteFragment { */ @Override protected String getContent() { - return binding.editContent.getText().toString(); + final Editable editable = binding.editContent.getText(); + return editable == null ? "" : editable.toString(); } @Override diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java index f1626149..f15d0e59 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java @@ -155,22 +155,22 @@ public class NotePreviewFragment extends SearchableBaseNoteFragment implements O public void onRefresh() { if (noteLoaded && repo.isSyncPossible() && SSOUtil.isConfigured(getContext())) { binding.swiperefreshlayout.setRefreshing(true); - new Thread(() -> { + executor.submit(() -> { try { final Account account = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()).name); - repo.addCallbackPull(account, () -> new Thread(() -> { + repo.addCallbackPull(account, () -> executor.submit(() -> { note = repo.getNoteById(note.getId()); changedText = note.getContent(); requireActivity().runOnUiThread(() -> { binding.singleNoteContent.setMarkdownString(note.getContent()); binding.swiperefreshlayout.setRefreshing(false); }); - }).start()); + })); repo.scheduleSync(account, false); } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { e.printStackTrace(); } - }).start(); + }); } else { binding.swiperefreshlayout.setRefreshing(false); Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java index c942b345..2e78af2c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java @@ -9,6 +9,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; +import androidx.preference.PreferenceManager; import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; @@ -20,14 +21,17 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.ui.UiExceptionManager; import java.net.HttpURLConnection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.ActivityImportAccountBinding; import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment; import it.niedermann.owncloud.notes.exception.ExceptionHandler; -import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; import it.niedermann.owncloud.notes.persistence.ApiProvider; +import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; +import it.niedermann.owncloud.notes.persistence.SyncWorker; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; @@ -37,6 +41,8 @@ public class ImportAccountActivity extends AppCompatActivity { private static final String TAG = ImportAccountActivity.class.getSimpleName(); public static final int REQUEST_CODE_IMPORT_ACCOUNT = 1; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private ImportAccountViewModel importAccountViewModel; private ActivityImportAccountBinding binding; @@ -84,12 +90,19 @@ public class ImportAccountActivity extends AppCompatActivity { runOnUiThread(() -> binding.progressCircular.setVisibility(View.VISIBLE)); SingleAccountHelper.setCurrentAccount(getApplicationContext(), ssoAccount.name); - new Thread(() -> { + executor.submit(() -> { Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); try { Log.i(TAG, "Loading capabilities for " + ssoAccount.name); - final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null); - importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() { + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance()); + final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()); + importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() { + + /** + * Update syncing when adding account + * https://github.com/stefan-niedermann/nextcloud-deck/issues/531 + * @param account the account to add + */ @Override public void onSuccess(Account account) { runOnUiThread(() -> { @@ -98,6 +111,8 @@ public class ImportAccountActivity extends AppCompatActivity { setResult(RESULT_OK); finish(); }); + SyncWorker.update(ImportAccountActivity.this, PreferenceManager.getDefaultSharedPreferences(ImportAccountActivity.this) + .getBoolean(getString(R.string.pref_key_background_sync), true)); } @Override @@ -110,7 +125,7 @@ public class ImportAccountActivity extends AppCompatActivity { }); } catch (Throwable t) { t.printStackTrace(); - ApiProvider.invalidateAPICache(ssoAccount); + ApiProvider.getInstance().invalidateAPICache(ssoAccount); SingleAccountHelper.setCurrentAccount(this, null); runOnUiThread(() -> { restoreCleanState(); @@ -120,7 +135,7 @@ public class ImportAccountActivity extends AppCompatActivity { } else if (t instanceof NetworkErrorException) { binding.status.setText(getString(R.string.error_sync, getString(R.string.error_no_network))); binding.status.setVisibility(View.VISIBLE); - } else if (t instanceof UnknownErrorException && t.getMessage().contains("No address associated with hostname")) { + } else if (t instanceof UnknownErrorException && t.getMessage() != null && t.getMessage().contains("No address associated with hostname")) { // https://github.com/stefan-niedermann/nextcloud-notes/issues/1014 binding.status.setText(R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account); binding.status.setVisibility(View.VISIBLE); @@ -129,7 +144,7 @@ public class ImportAccountActivity extends AppCompatActivity { } }); } - }).start(); + }); }); } catch (AccountImportCancelledException e) { restoreCleanState(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java index 905a59b1..70b3b565 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java @@ -3,6 +3,7 @@ package it.niedermann.owncloud.notes.importaccount; import android.app.Application; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; @@ -23,7 +24,7 @@ public class ImportAccountViewModel extends AndroidViewModel { this.repo = NotesRepository.getInstance(application); } - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) { - repo.addAccount(url, username, accountName, capabilities, callback); + public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + repo.addAccount(url, username, accountName, capabilities, displayName, callback); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index d889689d..83bf8e0b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -4,19 +4,18 @@ import android.accounts.NetworkErrorException; import android.animation.AnimatorInflater; import android.app.SearchManager; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.view.View; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.SearchView; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -49,6 +48,11 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper; import java.net.HttpURLConnection; import java.util.Collection; import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; import it.niedermann.owncloud.notes.LockedActivity; import it.niedermann.owncloud.notes.R; @@ -73,9 +77,9 @@ import it.niedermann.owncloud.notes.main.menu.MenuAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; +import it.niedermann.owncloud.notes.persistence.ApiProvider; import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; import it.niedermann.owncloud.notes.persistence.CapabilitiesWorker; -import it.niedermann.owncloud.notes.persistence.ApiProvider; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.Capabilities; @@ -83,7 +87,9 @@ import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.model.NoteClickListener; +import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule; import it.niedermann.owncloud.notes.shared.util.NoteUtil; +import it.niedermann.owncloud.notes.shared.util.ShareUtil; import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.O; @@ -92,7 +98,6 @@ import static android.view.View.VISIBLE; import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive; import static it.niedermann.owncloud.notes.NotesApplication.isGridViewEnabled; import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.owncloud.notes.main.menu.MenuAdapter.SERVER_SETTINGS; import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY; import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES; import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT; @@ -104,18 +109,19 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A private static final String TAG = MainActivity.class.getSimpleName(); + protected final ExecutorService executor = Executors.newCachedThreadPool(); + protected MainViewModel mainViewModel; private CategoryViewModel categoryViewModel; private boolean gridView = true; - 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"; public static final String ADAPTER_KEY_UNCATEGORIZED = "uncategorized"; - private final static int create_note_cmd = 0; - private final static int show_single_note_cmd = 1; + private static final int REQUEST_CODE_CREATE_NOTE = 0; + private static final int REQUEST_CODE_SERVER_SETTINGS = 1; protected ItemAdapter adapter; private NavigationAdapter adapterCategories; @@ -165,14 +171,47 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A if (count == 0) { startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); } else { - new Thread(() -> { + executor.submit(() -> { try { final Account account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name); runOnUiThread(() -> mainViewModel.postCurrentAccount(account)); - } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { + } catch (NextcloudFilesAppAccountNotFoundException e) { + // Verbose log output for https://github.com/stefan-niedermann/nextcloud-notes/issues/1256 + runOnUiThread(() -> new AlertDialog.Builder(this) + .setTitle(NextcloudFilesAppAccountNotFoundException.class.getSimpleName()) + .setMessage(R.string.backup) + .setPositiveButton(R.string.simple_backup, (a, b) -> executor.submit(() -> { + final List<Note> modifiedNotes = new LinkedList<>(); + for (Account account : mainViewModel.getAccounts()) { + modifiedNotes.addAll(mainViewModel.getLocalModifiedNotes(account.getId())); + } + if (modifiedNotes.size() == 1) { + final Note note = modifiedNotes.get(0); + ShareUtil.openShareDialog(this, note.getTitle(), note.getContent()); + } else { + ShareUtil.openShareDialog(this, + getResources().getQuantityString(R.plurals.share_multiple, modifiedNotes.size(), modifiedNotes.size()), + mainViewModel.collectNoteContents(modifiedNotes.stream().map(Note::getId).collect(Collectors.toList()))); + } + })) + .setNegativeButton(R.string.simple_error, (a, b) -> { + final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext()); + final StringBuilder ssoPreferencesString = new StringBuilder() + .append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n") + .append("\n") + .append("SSO SharedPreferences: ").append("\n"); + for (Map.Entry<String, ?> entry : ssoPreferences.getAll().entrySet()) { + ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); + } + ssoPreferencesString.append("\n") + .append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList()))); + runOnUiThread(() -> ExceptionDialogFragment.newInstance(new RuntimeException(e.getMessage(), new RuntimeException(ssoPreferencesString.toString(), e))).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); + }) + .show()); + } catch (NoCurrentAccountSelectedException e) { runOnUiThread(() -> ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } - }).start(); + }); } }); @@ -212,13 +251,13 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A } fabCreate.setOnClickListener((View view) -> { - Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class); + final Intent createIntent = new Intent(getApplicationContext(), EditNoteActivity.class); createIntent.putExtra(EditNoteActivity.PARAM_CATEGORY, selectedCategory); if (activityBinding.searchView.getQuery().length() > 0) { createIntent.putExtra(EditNoteActivity.PARAM_CONTENT, activityBinding.searchView.getQuery().toString()); invalidateOptionsMenu(); } - startActivityForResult(createIntent, create_note_cmd); + startActivityForResult(createIntent, REQUEST_CODE_CREATE_NOTE); }); }); mainViewModel.getNotesListLiveData().observe(this, notes -> { @@ -298,18 +337,18 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A activityBinding.launchAccountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance(nextAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName())); if (menuAdapter == null) { - menuAdapter = new MenuAdapter(getApplicationContext(), nextAccount, (menuItem) -> { + menuAdapter = new MenuAdapter(getApplicationContext(), nextAccount, REQUEST_CODE_SERVER_SETTINGS, (menuItem) -> { @Nullable Integer resultCode = menuItem.getResultCode(); if (resultCode == null) { startActivity(menuItem.getIntent()); } else { - startActivityForResult(menuItem.getIntent(), menuItem.getResultCode()); + startActivityForResult(menuItem.getIntent(), resultCode); } }); binding.navigationMenu.setAdapter(menuAdapter); } else { - menuAdapter.updateAccount(nextAccount); + menuAdapter.updateAccount(this, nextAccount); } }); } @@ -353,36 +392,14 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A setSupportActionBar(binding.activityNotesListView.toolbar); activityBinding.homeToolbar.setOnClickListener((v) -> { if (activityBinding.toolbar.getVisibility() == GONE) { - updateToolbars(false); + updateToolbars(true); } }); activityBinding.menuButton.setOnClickListener((v) -> binding.drawerLayout.openDrawer(GravityCompat.START)); - - final LinearLayout searchEditFrame = activityBinding.searchView.findViewById(R.id.search_edit_frame); - - searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - int oldVisibility = -1; - - @Override - public void onGlobalLayout() { - int currentVisibility = searchEditFrame.getVisibility(); - - if (currentVisibility != oldVisibility) { - if (currentVisibility == VISIBLE) { - fabCreate.hide(); - } else { - new Handler().postDelayed(() -> fabCreate.show(), 150); - } - - oldVisibility = currentVisibility; - } - } - - }); activityBinding.searchView.setOnCloseListener(() -> { if (activityBinding.toolbar.getVisibility() == VISIBLE && TextUtils.isEmpty(activityBinding.searchView.getQuery())) { - updateToolbars(true); + updateToolbars(false); return true; } return false; @@ -438,12 +455,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A }); swipeRefreshLayout.setOnRefreshListener(() -> { - Log.i(TAG, "Clearing Glide memory cache"); - Glide.get(this).clearMemory(); - new Thread(() -> { - Log.i(TAG, "Clearing Glide disk cache"); - Glide.get(getApplicationContext()).clearDiskCache(); - }, "CLEAR_GLIDE_CACHE").start(); + CustomAppGlideModule.clearCache(this); final LiveData<Account> syncLiveData = mainViewModel.getCurrentAccount(); final Observer<Account> syncObserver = currentAccount -> { syncLiveData.removeObservers(this); @@ -586,7 +598,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A @Override public boolean onSupportNavigateUp() { if (activityBinding.toolbar.getVisibility() == VISIBLE) { - updateToolbars(true); + updateToolbars(false); return true; } else { return super.onSupportNavigateUp(); @@ -638,33 +650,37 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { - case create_note_cmd: { + case REQUEST_CODE_CREATE_NOTE: { listView.scrollToPosition(0); break; } - case SERVER_SETTINGS: { + case REQUEST_CODE_SERVER_SETTINGS: { // Recreate activity completely, because theme switching makes problems when only invalidating the views. // @see https://github.com/stefan-niedermann/nextcloud-notes/issues/529 - ActivityCompat.recreate(this); + if (RESULT_OK == resultCode) { + ActivityCompat.recreate(this); + return; + } break; } default: { try { AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> { CapabilitiesWorker.update(this); - new Thread(() -> { + executor.submit(() -> { Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); try { Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); - final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null); - mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, new IResponseCallback<Account>() { + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance()); + final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance()); + mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<Account>() { @Override public void onSuccess(Account result) { - new Thread(() -> { + executor.submit(() -> { Log.i(TAG, capabilities.toString()); final Account a = mainViewModel.getLocalAccountByAccountName(ssoAccount.name); runOnUiThread(() -> mainViewModel.postCurrentAccount(a)); - }).start(); + }); } @Override @@ -673,7 +689,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A } }); } catch (Throwable e) { - ApiProvider.invalidateAPICache(ssoAccount); + ApiProvider.getInstance().invalidateAPICache(ssoAccount); // Happens when importing an already existing account the second time if (e instanceof TokenMismatchException && mainViewModel.getLocalAccountByAccountName(ssoAccount.name) != null) { Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported."); @@ -682,7 +698,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A // TODO there is already a sync in progress and results in displaying a TokenMissMatchException snackbar which conflicts with this one coordinatorLayout.post(() -> BrandedSnackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG).show()); }); - } else if (e instanceof UnknownErrorException && e.getMessage().contains("No address associated with hostname")) { + } else if (e instanceof UnknownErrorException && e.getMessage() != null && e.getMessage().contains("No address associated with hostname")) { // https://github.com/stefan-niedermann/nextcloud-notes/issues/1014 runOnUiThread(() -> Snackbar.make(coordinatorLayout, R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account, Snackbar.LENGTH_LONG).show()); } else { @@ -693,7 +709,7 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A }); } } - }).start(); + }); }); } catch (AccountImportCancelledException e) { Log.i(TAG, "AccountImport has been cancelled."); @@ -706,10 +722,9 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A public void onNoteClick(int position, View v) { boolean hasCheckedItems = tracker.getSelection().size() > 0; if (!hasCheckedItems) { - Note note = (Note) adapter.getItem(position); - Intent intent = new Intent(getApplicationContext(), EditNoteActivity.class); - intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); - startActivityForResult(intent, show_single_note_cmd); + final Note note = (Note) adapter.getItem(position); + startActivity(new Intent(getApplicationContext(), EditNoteActivity.class) + .putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId())); } } @@ -722,18 +737,24 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A @Override public void onBackPressed() { if (activityBinding.toolbar.getVisibility() == VISIBLE) { - updateToolbars(true); + updateToolbars(false); + } else if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + binding.drawerLayout.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } - private void updateToolbars(boolean disableSearch) { - activityBinding.homeToolbar.setVisibility(disableSearch ? VISIBLE : GONE); - activityBinding.toolbar.setVisibility(disableSearch ? GONE : VISIBLE); - activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(), - disableSearch ? R.animator.appbar_elevation_off : R.animator.appbar_elevation_on)); - if (disableSearch) { + private void updateToolbars(boolean enableSearch) { + activityBinding.homeToolbar.setVisibility(enableSearch ? GONE : VISIBLE); + activityBinding.toolbar.setVisibility(enableSearch ? VISIBLE : GONE); + activityBinding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(activityBinding.appBar.getContext(), enableSearch + ? R.animator.appbar_elevation_on + : R.animator.appbar_elevation_off)); + if (enableSearch) { + activityBinding.searchView.setIconified(false); + fabCreate.show(); + } else { activityBinding.searchView.setQuery(null, true); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java index ec0e71c7..92790bc4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java @@ -20,18 +20,23 @@ import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; +import it.niedermann.owncloud.notes.BuildConfig; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.exception.IntendedOfflineException; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; +import it.niedermann.owncloud.notes.persistence.ApiProvider; import it.niedermann.owncloud.notes.persistence.CapabilitiesClient; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; @@ -64,6 +69,8 @@ public class MainViewModel extends AndroidViewModel { private static final String TAG = MainViewModel.class.getSimpleName(); + private final ExecutorService executor = Executors.newCachedThreadPool(); + private final SavedStateHandle state; private static final String KEY_CURRENT_ACCOUNT = "currentAccount"; @@ -214,7 +221,7 @@ public class MainViewModel extends AndroidViewModel { } else { Log.v(TAG, "[getNotesListLiveData] - selectedCategory: " + selectedCategory); return switchMap(getSearchTerm(), searchTerm -> { - Log.v(TAG, "[getNotesListLiveData] - searchTerm: " + searchTerm); + Log.v(TAG, "[getNotesListLiveData] - searchTerm: " + (BuildConfig.DEBUG ? "******" : searchTerm)); return switchMap(getCategorySortingMethodOfSelectedCategory(), sortingMethod -> { final long accountId = currentAccount.getId(); final String searchQueryOrWildcard = searchTerm == null ? "%" : "%" + searchTerm.trim() + "%"; @@ -388,32 +395,35 @@ public class MainViewModel extends AndroidViewModel { * Updates the network status if necessary and pulls the latest {@link Capabilities} of the given {@param localAccount} */ public void synchronizeCapabilities(@NonNull Account localAccount, @NonNull IResponseCallback<Void> callback) { - new Thread(() -> { + executor.submit(() -> { if (!repo.isSyncPossible()) { repo.updateNetworkStatus(); } if (repo.isSyncPossible()) { try { - final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()), localAccount.getCapabilitiesETag()); - repo.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); - repo.updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor()); - localAccount.setColor(capabilities.getColor()); - localAccount.setTextColor(capabilities.getTextColor()); - BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor()); - repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); - callback.onSuccess(null); + final SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName()); + try { + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplication(), ssoAccount, localAccount.getCapabilitiesETag(), ApiProvider.getInstance()); + repo.updateCapabilitiesETag(localAccount.getId(), capabilities.getETag()); + repo.updateBrand(localAccount.getId(), capabilities.getColor(), capabilities.getTextColor()); + localAccount.setColor(capabilities.getColor()); + localAccount.setTextColor(capabilities.getTextColor()); + BrandingUtil.saveBrandColors(getApplication(), localAccount.getColor(), localAccount.getTextColor()); + repo.updateApiVersion(localAccount.getId(), capabilities.getApiVersion()); + callback.onSuccess(null); + } catch (Throwable t) { + if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) { + if (((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_NOT_MODIFIED) { + Log.d(TAG, "Server returned HTTP Status Code " + ((NextcloudHttpRequestFailedException) t).getStatusCode() + " - Capabilities not modified."); + callback.onSuccess(null); + return; + } + } + callback.onError(t); + } } catch (NextcloudFilesAppAccountNotFoundException e) { repo.deleteAccount(localAccount); callback.onError(e); - } catch (Throwable t) { - if (t.getClass() == NextcloudHttpRequestFailedException.class || t instanceof NextcloudHttpRequestFailedException) { - if (((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_NOT_MODIFIED) { - Log.d(TAG, "Server returned HTTP Status Code " + ((NextcloudHttpRequestFailedException) t).getStatusCode() + " - Capabilities not modified."); - callback.onSuccess(null); - return; - } - } - callback.onError(t); } } else { if (repo.isNetworkConnected() && repo.isSyncOnlyOnWifi()) { @@ -422,14 +432,14 @@ public class MainViewModel extends AndroidViewModel { callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected.")); } } - }, "SYNC_CAPABILITIES").start(); + }, "SYNC_CAPABILITIES"); } /** * Updates the network status if necessary and pulls the latest notes of the given {@param localAccount} */ public void synchronizeNotes(@NonNull Account currentAccount, @NonNull IResponseCallback<Void> callback) { - new Thread(() -> { + executor.submit(() -> { Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName()); if (!repo.isSyncPossible()) { repo.updateNetworkStatus(); @@ -444,7 +454,7 @@ public class MainViewModel extends AndroidViewModel { callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected.")); } } - }, "SYNC_NOTES").start(); + }, "SYNC_NOTES"); } public LiveData<Boolean> getSyncStatus() { @@ -483,9 +493,9 @@ public class MainViewModel extends AndroidViewModel { }); } - public LiveData<Note> moveNoteToAnotherAccount(Account account, Long noteId) { + public LiveData<Note> moveNoteToAnotherAccount(Account account, long noteId) { return switchMap(repo.getNoteById$(noteId), (note) -> { - Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + note); + Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + (BuildConfig.DEBUG ? note : note.getTitle())); return repo.moveNoteToAnotherAccount(account, note); }); } @@ -528,8 +538,8 @@ public class MainViewModel extends AndroidViewModel { }); } - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) { - repo.addAccount(url, username, accountName, capabilities, callback); + public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + repo.addAccount(url, username, accountName, capabilities, displayName, callback); } public LiveData<Note> getFullNote$(long id) { @@ -548,12 +558,12 @@ public class MainViewModel extends AndroidViewModel { } else { Log.v(TAG, "[getNote] - currentAccount: " + currentAccount.getAccountName()); final MutableLiveData<List<Note>> notes = new MutableLiveData<>(); - new Thread(() -> notes.postValue( + executor.submit(() -> notes.postValue( ids .stream() .map(repo::getNoteById) .collect(Collectors.toList()) - )).start(); + )); return notes; } }); @@ -584,6 +594,10 @@ public class MainViewModel extends AndroidViewModel { repo.createOrUpdateSingleNoteWidgetData(data); } + public List<Note> getLocalModifiedNotes(long accountId) { + return repo.getLocalModifiedNotes(accountId); + } + public LiveData<Integer> getAccountsCount() { return repo.countAccounts$(); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java index fa9bb879..cb51cdfc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java @@ -21,6 +21,8 @@ import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment; @@ -32,6 +34,7 @@ import it.niedermann.owncloud.notes.shared.util.ShareUtil; public class MultiSelectedActionModeCallback implements Callback { + private final ExecutorService executor = Executors.newSingleThreadExecutor(); @ColorInt private final int colorAccent; @NonNull @@ -125,11 +128,9 @@ public class MultiSelectedActionModeCallback implements Callback { final LiveData<Account> currentAccount$ = mainViewModel.getCurrentAccount(); currentAccount$.observe(lifecycleOwner, account -> { currentAccount$.removeObservers(lifecycleOwner); - new Thread(() -> { - AccountPickerDialogFragment - .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId()) - .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName()); - }).start(); + executor.submit(() -> AccountPickerDialogFragment + .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId()) + .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName())); }); return true; } else if (itemId == R.id.menu_share) { @@ -139,7 +140,7 @@ public class MultiSelectedActionModeCallback implements Callback { } tracker.clearSelection(); - new Thread(() -> { + executor.submit(() -> { if (selection.size() == 1) { final Note note = mainViewModel.getFullNote(selection.get(0)); ShareUtil.openShareDialog(context, note.getTitle(), note.getContent()); @@ -148,7 +149,7 @@ public class MultiSelectedActionModeCallback implements Callback { context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()), mainViewModel.collectNoteContents(selection)); } - }).start(); + }); return true; } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it final LiveData<Account> accountLiveData = mainViewModel.getCurrentAccount(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java index 2cfcee88..01743638 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java @@ -20,6 +20,9 @@ public class GridItemDecoration extends SectionItemDecoration { public GridItemDecoration(@NonNull ItemAdapter adapter, int spanCount, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom, @Px int gutter) { super(adapter, sectionLeft, sectionTop, sectionRight, sectionBottom); + if(spanCount < 1) { + throw new IllegalArgumentException("Requires at least one span"); + } this.spanCount = spanCount; this.adapter = adapter; this.gutter = gutter; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java index 82d88d11..24c1a44e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java @@ -2,15 +2,18 @@ package it.niedermann.owncloud.notes.main.menu; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; +import com.nextcloud.android.sso.Constants; +import com.nextcloud.android.sso.helper.VersionCheckHelper; + import it.niedermann.owncloud.notes.FormattingHelpActivity; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.about.AboutActivity; @@ -20,21 +23,16 @@ import it.niedermann.owncloud.notes.preferences.PreferencesActivity; public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> { - public static final int SERVER_SETTINGS = 2; - @NonNull private final MenuItem[] menuItems; @NonNull private final Consumer<MenuItem> onClick; - @NonNull - private final Context context; - public MenuAdapter(@NonNull Context context, @NonNull Account account, @NonNull Consumer<MenuItem> onClick) { - this.context = context; + public MenuAdapter(@NonNull Context context, @NonNull Account account, int settingsRequestCode, @NonNull Consumer<MenuItem> onClick) { this.menuItems = new MenuItem[]{ new MenuItem(new Intent(context, FormattingHelpActivity.class), R.string.action_formatting_help, R.drawable.ic_baseline_help_outline_24), - new MenuItem(generateTrashbinIntent(account), R.string.action_trashbin, R.drawable.ic_delete_grey600_24dp), - new MenuItem(new Intent(context, PreferencesActivity.class), SERVER_SETTINGS, R.string.action_settings, R.drawable.ic_settings_grey600_24dp), + new MenuItem(generateTrashbinIntent(context, account), R.string.action_trashbin, R.drawable.ic_delete_grey600_24dp), + new MenuItem(new Intent(context, PreferencesActivity.class), settingsRequestCode, R.string.action_settings, R.drawable.ic_settings_grey600_24dp), new MenuItem(new Intent(context, AboutActivity.class), R.string.simple_about, R.drawable.ic_info_outline_grey600_24dp) }; this.onClick = onClick; @@ -54,11 +52,11 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> { @Override public void onBindViewHolder(@NonNull MenuViewHolder holder, int position) { - holder.bind(menuItems[position], onClick, ContextCompat.getColor(context, R.color.fg_default)); + holder.bind(menuItems[position], onClick); } - public void updateAccount(@NonNull Account account) { - menuItems[1].setIntent(new Intent(generateTrashbinIntent(account))); + public void updateAccount(@NonNull Context context, @NonNull Account account) { + menuItems[1].setIntent(new Intent(generateTrashbinIntent(context, account))); } @Override @@ -67,7 +65,38 @@ public class MenuAdapter extends RecyclerView.Adapter<MenuViewHolder> { } @NonNull - private static Intent generateTrashbinIntent(@NonNull Account account) { + private static Intent generateTrashbinIntent(@NonNull Context context, @NonNull Account account) { + // https://github.com/nextcloud/android/pull/8405#issuecomment-852966877 + final int minVersionCode = 30170090; + try { + if (VersionCheckHelper.getNextcloudFilesVersionCode(context, true) > minVersionCode) { + return generateTrashbinAppIntent(context, account, true); + } else if (VersionCheckHelper.getNextcloudFilesVersionCode(context, false) > minVersionCode) { + return generateTrashbinAppIntent(context, account, false); + } else { + // Files app is too old to be able to switch the account when launching the TrashbinActivity + return generateTrashbinWebIntent(account); + } + } catch (PackageManager.NameNotFoundException | SecurityException e) { + e.printStackTrace(); + return generateTrashbinWebIntent(account); + } + } + + private static Intent generateTrashbinAppIntent(@NonNull Context context, @NonNull Account account, boolean prod) throws PackageManager.NameNotFoundException { + final PackageManager packageManager = context.getPackageManager(); + final String packageName = prod ? Constants.PACKAGE_NAME_PROD : Constants.PACKAGE_NAME_DEV; + final Intent intent = new Intent(); + intent.setClassName(packageName, "com.owncloud.android.ui.trashbin.TrashbinActivity"); + if (packageManager.resolveActivity(intent, 0) != null) { + return intent + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(Intent.EXTRA_USER, account.getAccountName()); + } + throw new PackageManager.NameNotFoundException("Could not resolve target activity."); + } + + private static Intent generateTrashbinWebIntent(@NonNull Account account) { return new Intent(Intent.ACTION_VIEW, Uri.parse(account.getUrl() + "/index.php/apps/files/?dir=/&view=trashbin")); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java index 0e08e279..d5a2f608 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuViewHolder.java @@ -2,12 +2,12 @@ package it.niedermann.owncloud.notes.main.menu; import android.content.Context; -import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding; import static android.view.View.GONE; @@ -21,10 +21,10 @@ public class MenuViewHolder extends RecyclerView.ViewHolder { this.binding = binding; } - public void bind(@NonNull MenuItem menuItem, @NonNull Consumer<MenuItem> onClick, @ColorInt int textColor) { + public void bind(@NonNull MenuItem menuItem, @NonNull Consumer<MenuItem> onClick) { @NonNull Context context = itemView.getContext(); binding.navigationItemLabel.setText(context.getString(menuItem.getLabelResource())); - binding.navigationItemLabel.setTextColor(textColor); + binding.navigationItemLabel.setTextColor(binding.getRoot().getResources().getColor(R.color.fg_default)); binding.navigationItemIcon.setImageDrawable(ContextCompat.getDrawable(context, menuItem.getDrawableResource())); binding.navigationItemCount.setVisibility(GONE); binding.getRoot().setOnClickListener((v) -> onClick.accept(menuItem)); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java index 2ee45cf8..ce88f2c2 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/manageaccounts/ManageAccountsViewModel.java @@ -13,8 +13,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -import it.niedermann.owncloud.notes.persistence.NotesDatabase; import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.shared.model.IResponseCallback; @@ -23,7 +24,7 @@ import static androidx.lifecycle.Transformations.distinctUntilChanged; public class ManageAccountsViewModel extends AndroidViewModel { - private static final String TAG = ManageAccountsViewModel.class.getSimpleName(); + private final ExecutorService executor = Executors.newCachedThreadPool(); @NonNull private final NotesRepository repo; @@ -46,7 +47,7 @@ public class ManageAccountsViewModel extends AndroidViewModel { } public void deleteAccount(@NonNull Account account, @NonNull Context context) { - new Thread(() -> { + executor.submit(() -> { final List<Account> accounts = repo.getAccounts(); for (int i = 0; i < accounts.size(); i++) { if (accounts.get(i).getId() == account.getId()) { @@ -61,7 +62,7 @@ public class ManageAccountsViewModel extends AndroidViewModel { break; } } - }).start(); + }); } public void selectAccount(@Nullable Account account, @NonNull Context context) { @@ -69,6 +70,6 @@ public class ManageAccountsViewModel extends AndroidViewModel { } public void countUnsynchronizedNotes(long accountId, @NonNull IResponseCallback<Long> callback) { - new Thread(() -> callback.onSuccess(repo.countUnsynchronizedNotes(accountId))).start(); + executor.submit(() -> callback.onSuccess(repo.countUnsynchronizedNotes(accountId))); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index b1bca215..6b851ddb 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -16,8 +16,8 @@ import com.nextcloud.android.sso.api.NextcloudAPI; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.util.Calendar; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import it.niedermann.owncloud.notes.persistence.sync.CapabilitiesDeserializer; import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; @@ -36,17 +36,27 @@ public class ApiProvider { private static final String TAG = ApiProvider.class.getSimpleName(); + private static final ApiProvider INSTANCE = new ApiProvider(); + private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/"; - private static final Map<String, NextcloudAPI> API_CACHE = new HashMap<>(); + private static final Map<String, NextcloudAPI> API_CACHE = new ConcurrentHashMap<>(); + + private static final Map<String, OcsAPI> API_CACHE_OCS = new ConcurrentHashMap<>(); + private static final Map<String, NotesAPI> API_CACHE_NOTES = new ConcurrentHashMap<>(); - private static final Map<String, OcsAPI> API_CACHE_OCS = new HashMap<>(); - private static final Map<String, NotesAPI> API_CACHE_NOTES = new HashMap<>(); + public static ApiProvider getInstance() { + return INSTANCE; + } + + private ApiProvider() { + // Singleton + } /** * An {@link OcsAPI} currently shares the {@link Gson} configuration with the {@link NotesAPI} and therefore divides all {@link Calendar} milliseconds by 1000 while serializing and multiplies values by 1000 during deserialization. */ - public static synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { + public synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { if (API_CACHE_OCS.containsKey(ssoAccount.name)) { return API_CACHE_OCS.get(ssoAccount.name); } @@ -58,7 +68,7 @@ public class ApiProvider { /** * In case the {@param preferredApiVersion} changes, call {@link #invalidateAPICache(SingleSignOnAccount)} or {@link #invalidateAPICache()} to make sure that this call returns a {@link NotesAPI} that uses the correct compatibility layer. */ - public static synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) { + public synchronized NotesAPI getNotesAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable ApiVersion preferredApiVersion) { if (API_CACHE_NOTES.containsKey(ssoAccount.name)) { return API_CACHE_NOTES.get(ssoAccount.name); } @@ -67,7 +77,7 @@ public class ApiProvider { return notesAPI; } - private static synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { + private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { if (API_CACHE.containsKey(ssoAccount.name)) { return API_CACHE.get(ssoAccount.name); } else { @@ -104,7 +114,7 @@ public class ApiProvider { * * @param ssoAccount the ssoAccount for which the API cache should be cleared. */ - public static synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) { + public synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAccount) { Log.v(TAG, "Invalidating API cache for " + ssoAccount.name); if (API_CACHE.containsKey(ssoAccount.name)) { final NextcloudAPI nextcloudAPI = API_CACHE.get(ssoAccount.name); @@ -120,7 +130,7 @@ public class ApiProvider { /** * Invalidates the whole API cache for all accounts */ - public static synchronized void invalidateAPICache() { + public synchronized void invalidateAPICache() { for (String key : API_CACHE.keySet()) { Log.v(TAG, "Invalidating API cache for " + key); if (API_CACHE.containsKey(key)) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java index 8afc64b8..33e42382 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java @@ -14,6 +14,9 @@ import java.util.Map; import it.niedermann.owncloud.notes.persistence.sync.OcsAPI; import it.niedermann.owncloud.notes.shared.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.OcsResponse; +import it.niedermann.owncloud.notes.shared.model.OcsUser; +import retrofit2.Response; @WorkerThread public class CapabilitiesClient { @@ -22,11 +25,12 @@ public class CapabilitiesClient { private static final String HEADER_KEY_ETAG = "ETag"; - public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag) throws Throwable { - final OcsAPI ocsAPI = ApiProvider.getOcsAPI(context, ssoAccount); + @WorkerThread + public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag, @NonNull ApiProvider apiProvider) throws Throwable { + final OcsAPI ocsAPI = apiProvider.getOcsAPI(context, ssoAccount); try { - final ParsedResponse<Capabilities> response = ocsAPI.getCapabilities(lastETag).blockingSingle(); - final Capabilities capabilities = response.getResponse(); + final ParsedResponse<OcsResponse<Capabilities>> response = ocsAPI.getCapabilities(lastETag).blockingSingle(); + final Capabilities capabilities = response.getResponse().ocs.data; final Map<String, String> headers = response.getHeaders(); if (headers != null) { capabilities.setETag(headers.get(HEADER_KEY_ETAG)); @@ -36,11 +40,33 @@ public class CapabilitiesClient { return capabilities; } catch (RuntimeException e) { final Throwable cause = e.getCause(); - if(cause != null) { + if (cause != null) { throw cause; } else { throw e; } } } + + @WorkerThread + @Nullable + public static String getDisplayName(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @NonNull ApiProvider apiProvider) { + final OcsAPI ocsAPI = apiProvider.getOcsAPI(context, ssoAccount); + try { + final Response<OcsResponse<OcsUser>> userResponse = ocsAPI.getUser(ssoAccount.userId).execute(); + if (userResponse.isSuccessful()) { + final OcsResponse<OcsUser> ocsResponse = userResponse.body(); + if (ocsResponse != null) { + return ocsResponse.ocs.data.displayName; + } else { + Log.w(TAG, "ocsResponse is null"); + } + } else { + Log.w(TAG, "Fetching user was not successful."); + } + } catch (Throwable t) { + t.printStackTrace(); + } + return null; + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java index 1dff46cf..4f8852e7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesWorker.java @@ -47,17 +47,18 @@ public class CapabilitiesWorker extends Worker { try { final SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(getApplicationContext(), account.getAccountName()); Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name); - final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag()); + final Capabilities capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, account.getCapabilitiesETag(), ApiProvider.getInstance()); repo.updateCapabilitiesETag(account.getId(), capabilities.getETag()); repo.updateBrand(account.getId(), capabilities.getColor(), capabilities.getTextColor()); repo.updateApiVersion(account.getId(), capabilities.getApiVersion()); Log.i(TAG, capabilities.toString()); + repo.updateDisplayName(account.getId(), CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance())); } catch (Throwable e) { if (e instanceof NextcloudHttpRequestFailedException) { if (((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { Log.i(TAG, "Capabilities not modified."); return Result.success(); - } else if(((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) { + } else if (((NextcloudHttpRequestFailedException) e).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) { Log.i(TAG, "Server is in maintenance mode."); return Result.success(); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java index 4f4aaa78..5cc50641 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java @@ -32,6 +32,8 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_17_18; import it.niedermann.owncloud.notes.persistence.migration.Migration_18_19; import it.niedermann.owncloud.notes.persistence.migration.Migration_19_20; import it.niedermann.owncloud.notes.persistence.migration.Migration_20_21; +import it.niedermann.owncloud.notes.persistence.migration.Migration_21_22; +import it.niedermann.owncloud.notes.persistence.migration.Migration_22_23; import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10; @Database( @@ -41,7 +43,7 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10; CategoryOptions.class, SingleNoteWidgetData.class, NotesListWidgetData.class - }, version = 21 + }, version = 23 ) @TypeConverters({Converters.class}) public abstract class NotesDatabase extends RoomDatabase { @@ -74,7 +76,9 @@ public abstract class NotesDatabase extends RoomDatabase { new Migration_17_18(), new Migration_18_19(context), new Migration_19_20(context), - new Migration_20_21() + new Migration_20_21(), + new Migration_21_22(context), + new Migration_22_23() ) .fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigration() diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index afd6145d..f36fd72a 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -30,11 +30,9 @@ import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; -import org.json.JSONArray; -import org.json.JSONException; - import java.util.ArrayList; import java.util.Calendar; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -44,6 +42,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData; +import it.niedermann.owncloud.notes.BuildConfig; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.edit.EditNoteActivity; import it.niedermann.owncloud.notes.persistence.entity.Account; @@ -62,6 +61,7 @@ import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; import it.niedermann.owncloud.notes.shared.model.NotesSettings; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; +import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.SSOUtil; import retrofit2.Call; @@ -70,7 +70,6 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.O; import static androidx.lifecycle.Transformations.distinctUntilChanged; import static androidx.lifecycle.Transformations.map; -import static androidx.lifecycle.Transformations.switchMap; import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt; import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets; @@ -84,6 +83,7 @@ public class NotesRepository { private static NotesRepository instance; + private final ApiProvider apiProvider; private final ExecutorService executor; private final Context context; private final NotesDatabase db; @@ -137,22 +137,23 @@ public class NotesRepository { public static synchronized NotesRepository getInstance(@NonNull Context context) { if (instance == null) { - instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool()); + instance = new NotesRepository(context, NotesDatabase.getInstance(context.getApplicationContext()), Executors.newCachedThreadPool(), ApiProvider.getInstance()); } return instance; } - private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor) { + private NotesRepository(@NonNull final Context context, @NonNull final NotesDatabase db, @NonNull final ExecutorService executor, @NonNull ApiProvider apiProvider) { this.context = context.getApplicationContext(); this.db = db; this.executor = executor; + this.apiProvider = apiProvider; this.defaultNonEmptyTitle = NoteUtil.generateNonEmptyNoteTitle("", this.context); this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only); // Registers BroadcastReceiver to track network connection changes. - context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + this.context.registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext()); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context); prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener); syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false); @@ -163,8 +164,8 @@ public class NotesRepository { // Accounts @AnyThread - public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @NonNull IResponseCallback<Account> callback) { - final Account createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, capabilities))); + public void addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback<Account> callback) { + final Account createdAccount = db.getAccountDao().getAccountById(db.getAccountDao().insert(new Account(url, username, accountName, displayName, capabilities))); if (createdAccount == null) { callback.onError(new Exception("Could not read created account.")); } else { @@ -180,10 +181,10 @@ public class NotesRepository { @WorkerThread public void deleteAccount(@NonNull Account account) { try { - ApiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName())); + apiProvider.invalidateAPICache(AccountImporter.getSingleSignOnAccount(context, account.getAccountName())); } catch (NextcloudFilesAppAccountNotFoundException e) { e.printStackTrace(); - ApiProvider.invalidateAPICache(); + apiProvider.invalidateAPICache(); } db.getAccountDao().deleteAccount(account); @@ -403,10 +404,12 @@ public class NotesRepository { @MainThread public LiveData<Note> moveNoteToAnotherAccount(Account account, @NonNull Note note) { - return switchMap(db.getNoteDao().getContent$(note.getId()), (content) -> { - final Note fullNote = new Note(null, note.getModified(), note.getTitle(), content, note.getCategory(), note.getFavorite(), null); - deleteNoteAndSync(account, note.getId()); - return addNoteAndSync(account, fullNote); + final Note fullNote = new Note(null, note.getModified(), note.getTitle(), note.getContent(), note.getCategory(), note.getFavorite(), null); + deleteNoteAndSync(account, note.getId()); + return map(addNoteAndSync(account, fullNote), (createdNote) -> { + db.getNoteDao().updateStatus(createdNote.getId(), DBStatus.LOCAL_EDITED); + createdNote.setStatus(DBStatus.LOCAL_EDITED); + return createdNote; }); } @@ -460,23 +463,27 @@ public class NotesRepository { * @return changed {@link Note} if differs from database, otherwise the old {@link Note}. */ @WorkerThread - public Note updateNoteAndSync(Account localAccount, @NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) { + public Note updateNoteAndSync(@NonNull Account localAccount, @NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle, @Nullable ISyncCallback callback) { final Note newNote; + // Re-read the up to date remoteId from the database because the UI might not have the state after synchronization yet + // https://github.com/stefan-niedermann/nextcloud-notes/issues/1198 + @Nullable final Long remoteId = db.getNoteDao().getRemoteId(oldNote.getId()); if (newContent == null) { - newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY()); + newNote = new Note(oldNote.getId(), remoteId, oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), oldNote.getExcerpt(), oldNote.getScrollY()); } else { final String title; if (newTitle != null) { title = newTitle; } else { - if ((oldNote.getRemoteId() == null || localAccount.getPreferredApiVersion() == null || localAccount.getPreferredApiVersion().compareTo(ApiVersion.API_VERSION_1_0) < 0) && + final ApiVersion preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion()); + if ((remoteId == null || preferredApiVersion == null || preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) < 0) && (defaultNonEmptyTitle.equals(oldNote.getTitle()))) { title = NoteUtil.generateNonEmptyNoteTitle(newContent, context); } else { title = oldNote.getTitle(); } } - newNote = new Note(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY()); + newNote = new Note(oldNote.getId(), remoteId, Calendar.getInstance(), title, newContent, oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag(), DBStatus.LOCAL_EDITED, localAccount.getId(), generateNoteExcerpt(newContent, title), oldNote.getScrollY()); } int rows = db.getNoteDao().updateNote(newNote); // if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly. @@ -540,25 +547,25 @@ public class NotesRepository { private void updateDynamicShortcuts(long accountId) { executor.submit(() -> { if (SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) { - ShortcutManager shortcutManager = context.getApplicationContext().getSystemService(ShortcutManager.class); + ShortcutManager shortcutManager = this.context.getSystemService(ShortcutManager.class); if (shortcutManager != null) { if (!shortcutManager.isRateLimitingActive()) { List<ShortcutInfo> newShortcuts = new ArrayList<>(); for (Note note : db.getNoteDao().getRecentNotes(accountId)) { if (!TextUtils.isEmpty(note.getTitle())) { - Intent intent = new Intent(context.getApplicationContext(), EditNoteActivity.class); + Intent intent = new Intent(this.context, EditNoteActivity.class); intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()); intent.setAction(ACTION_SHORTCUT); - newShortcuts.add(new ShortcutInfo.Builder(context.getApplicationContext(), note.getId() + "") + newShortcuts.add(new ShortcutInfo.Builder(this.context, note.getId() + "") .setShortLabel(note.getTitle() + "") - .setIcon(Icon.createWithResource(context.getApplicationContext(), note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) + .setIcon(Icon.createWithResource(this.context, note.getFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp)) .setIntent(intent) .build()); } else { // Prevent crash https://github.com/stefan-niedermann/nextcloud-notes/issues/613 - Log.e(TAG, "shortLabel cannot be empty " + note); + Log.e(TAG, "shortLabel cannot be empty " + (BuildConfig.DEBUG ? note : note.getTitle())); } } Log.d(TAG, "Update dynamic shortcuts"); @@ -571,40 +578,23 @@ public class NotesRepository { } /** - * @param apiVersion has to be a JSON array as a string <code>["0.2", "1.0", ...]</code> - * @return whether or not the given {@link ApiVersion} has been written to the database - * @throws IllegalArgumentException if the apiVersion does not match the expected format + * @param raw has to be a JSON array as a string <code>["0.2", "1.0", ...]</code> */ - public boolean updateApiVersion(long accountId, @Nullable String apiVersion) throws IllegalArgumentException { - if (apiVersion != null) { - try { - JSONArray apiVersions = new JSONArray(apiVersion); - for (int i = 0; i < apiVersions.length(); i++) { - ApiVersion.of(apiVersions.getString(i)); - } - if (apiVersions.length() > 0) { - final int updatedRows = db.getAccountDao().updateApiVersion(accountId, apiVersion); - if (updatedRows == 0) { - Log.d(TAG, "ApiVersion not updated, because it did not change"); - } else if (updatedRows == 1) { - Log.i(TAG, "Updated apiVersion to \"" + apiVersion + "\" for accountId = " + accountId); - ApiProvider.invalidateAPICache(); - } else { - Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + apiVersion + "\""); - } - return true; - } else { - Log.i(TAG, "Given API version is a valid JSON array but does not contain any valid API versions. Do not update database."); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("API version does contain a non-valid version: " + apiVersion); - } catch (JSONException e) { - throw new IllegalArgumentException("API version must contain be a JSON array: " + apiVersion); + public void updateApiVersion(long accountId, @Nullable String raw) { + final Collection<ApiVersion> apiVersions = ApiVersionUtil.parse(raw); + if (apiVersions.size() > 0) { + final int updatedRows = db.getAccountDao().updateApiVersion(accountId, ApiVersionUtil.serialize(apiVersions)); + if (updatedRows == 0) { + Log.d(TAG, "ApiVersion not updated, because it did not change"); + } else if (updatedRows == 1) { + Log.i(TAG, "Updated apiVersion to \"" + raw + "\" for accountId = " + accountId); + apiProvider.invalidateAPICache(); + } else { + Log.w(TAG, "Updated " + updatedRows + " but expected only 1 for accountId = " + accountId + " and apiVersion = \"" + raw + "\""); } } else { - Log.v(TAG, "Given API version is null. Do not update database"); + Log.v(TAG, "Could not extract any version from the given String: " + raw); } - return false; } /** @@ -709,7 +699,7 @@ public class NotesRepository { @Override protected void finalize() throws Throwable { - context.getApplicationContext().unregisterReceiver(networkReceiver); + this.context.unregisterReceiver(networkReceiver); super.finalize(); } @@ -783,7 +773,7 @@ public class NotesRepository { * * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server. */ - public synchronized void scheduleSync(Account account, boolean onlyLocalChanges) { + public synchronized void scheduleSync(@Nullable Account account, boolean onlyLocalChanges) { if (account == null) { Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?"); } else { @@ -795,7 +785,7 @@ public class NotesRepository { syncActive.put(account.getId(), true); try { Log.d(TAG, "... starting now"); - final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, onlyLocalChanges) { + final NotesServerSyncTask syncTask = new NotesServerSyncTask(context, this, account, onlyLocalChanges, apiProvider) { @Override void onPreExecute() { syncStatus.postValue(true); @@ -873,7 +863,7 @@ public class NotesRepository { public void updateNetworkStatus() { try { - final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + final ConnectivityManager connMgr = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connMgr == null) { throw new NetworkErrorException("ConnectivityManager is null"); } @@ -927,4 +917,8 @@ public class NotesRepository { public Call<NotesSettings> putServerSettings(@NonNull SingleSignOnAccount ssoAccount, @NonNull NotesSettings settings, @Nullable ApiVersion preferredApiVersion) { return ApiProvider.getNotesAPI(context, ssoAccount, preferredApiVersion).putSettings(settings); } + + public void updateDisplayName(long id, @Nullable String displayName) { + db.getAccountDao().updateDisplayName(id, displayName); + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java index 88fb44f3..a7df68bd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.api.ParsedResponse; +import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; @@ -19,15 +20,16 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; +import it.niedermann.owncloud.notes.BuildConfig; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; +import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; import retrofit2.Response; import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED; @@ -51,6 +53,8 @@ abstract class NotesServerSyncTask extends Thread { private NotesAPI notesAPI; @NonNull + private final ApiProvider apiProvider; + @NonNull private final Context context; @NonNull private final NotesRepository repo; @@ -64,13 +68,14 @@ abstract class NotesServerSyncTask extends Thread { @NonNull protected final ArrayList<Throwable> exceptions = new ArrayList<>(); - NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges) throws NextcloudFilesAppAccountNotFoundException { + NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges, @NonNull ApiProvider apiProvider) throws NextcloudFilesAppAccountNotFoundException { super(TAG); this.context = context; this.repo = repo; this.localAccount = localAccount; this.ssoAccount = AccountImporter.getSingleSignOnAccount(context, localAccount.getAccountName()); this.onlyLocalChanges = onlyLocalChanges; + this.apiProvider = apiProvider; } void addCallbacks(Account account, List<ISyncCallback> callbacks) { @@ -81,7 +86,7 @@ abstract class NotesServerSyncTask extends Thread { public void run() { onPreExecute(); - notesAPI = ApiProvider.getNotesAPI(context, ssoAccount, localAccount.getPreferredApiVersion()); + notesAPI = apiProvider.getNotesAPI(context, ssoAccount, ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion())); Log.i(TAG, "STARTING SYNCHRONIZATION"); @@ -109,7 +114,7 @@ abstract class NotesServerSyncTask extends Thread { boolean success = true; final List<Note> notes = repo.getLocalModifiedNotes(localAccount.getId()); for (Note note : notes) { - Log.d(TAG, " Process Local Note: " + note); + Log.d(TAG, " Process Local Note: " + (BuildConfig.DEBUG ? note : note.getTitle())); try { Note remoteNote; switch (note.getStatus()) { @@ -120,27 +125,37 @@ abstract class NotesServerSyncTask extends Thread { final Response<Note> editResponse = notesAPI.editNote(note).execute(); if (editResponse.isSuccessful()) { remoteNote = editResponse.body(); - } else { - if (editResponse.code() == HTTP_NOT_FOUND) { - Log.v(TAG, " ...Note does no longer exist on server → recreate"); - final Response<Note> createResponse = notesAPI.createNote(note).execute(); - if (createResponse.isSuccessful()) { - remoteNote = createResponse.body(); - } else { - throw new Exception(createResponse.errorBody().string()); + if (remoteNote == null) { + Log.e(TAG, " ...Tried to edit \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null."); + throw new Exception("Server returned null after editing \"" + note.getTitle() + "\" (#" + note.getId() + ")"); + } + } else if (editResponse.code() == HTTP_NOT_FOUND) { + Log.v(TAG, " ...Note does no longer exist on server → recreate"); + final Response<Note> createResponse = notesAPI.createNote(note).execute(); + if (createResponse.isSuccessful()) { + remoteNote = createResponse.body(); + if (remoteNote == null) { + Log.e(TAG, " ...Tried to recreate \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null."); + throw new Exception("Server returned null after recreating \"" + note.getTitle() + "\" (#" + note.getId() + ")"); } } else { - throw new Exception(editResponse.errorBody().string()); + throw new Exception(createResponse.message()); } + } else { + throw new Exception(editResponse.message()); } } else { Log.v(TAG, " ...Note does not have a remoteId yet → create"); final Response<Note> createResponse = notesAPI.createNote(note).execute(); if (createResponse.isSuccessful()) { remoteNote = createResponse.body(); + if (remoteNote == null) { + Log.e(TAG, " ...Tried to create \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null."); + throw new Exception("Server returned null after creating \"" + note.getTitle() + "\" (#" + note.getId() + ")"); + } repo.updateRemoteId(note.getId(), remoteNote.getRemoteId()); } else { - throw new Exception(createResponse.errorBody().string()); + throw new Exception(createResponse.message()); } } // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI. @@ -156,7 +171,7 @@ abstract class NotesServerSyncTask extends Thread { if (deleteResponse.code() == HTTP_NOT_FOUND) { Log.v(TAG, " ...delete (note has already been deleted remotely)"); } else { - throw new Exception(deleteResponse.errorBody().string()); + throw new Exception(deleteResponse.message()); } } } @@ -175,7 +190,7 @@ abstract class NotesServerSyncTask extends Thread { } } catch (Exception e) { if (e instanceof TokenMismatchException) { - ApiProvider.invalidateAPICache(ssoAccount); + apiProvider.invalidateAPICache(ssoAccount); } exceptions.add(e); success = false; @@ -206,7 +221,7 @@ abstract class NotesServerSyncTask extends Thread { final Set<Long> remoteIDs = new HashSet<>(); // pull remote changes: update or create each remote note for (Note remoteNote : remoteNotes) { - Log.v(TAG, " Process Remote Note: " + remoteNote); + Log.v(TAG, " Process Remote Note: " + (BuildConfig.DEBUG ? remoteNote : remoteNote.getTitle())); remoteIDs.add(remoteNote.getRemoteId()); if (remoteNote.getModified() == null) { Log.v(TAG, " ... unchanged"); @@ -217,7 +232,7 @@ abstract class NotesServerSyncTask extends Thread { repo.updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged( localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle())); } else { - Log.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote); + Log.e(TAG, "Tried to update note from server, but local id of note is null. " + (BuildConfig.DEBUG ? remoteNote : remoteNote.getTitle())); } } else { Log.v(TAG, " ... create"); @@ -248,19 +263,10 @@ abstract class NotesServerSyncTask extends Thread { repo.updateETag(localAccount.getId(), localAccount.getETag()); repo.updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis()); - - String supportedApiVersions = null; - final String supportedApiVersionsHeader = fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS); - if (supportedApiVersionsHeader != null) { - supportedApiVersions = "[" + Objects.requireNonNull(supportedApiVersionsHeader) + "]"; - } - try { - if (repo.updateApiVersion(localAccount.getId(), supportedApiVersions)) { - localAccount.setApiVersion(supportedApiVersions); - } - } catch (Exception e) { - exceptions.add(e); - } + final String newApiVersion = ApiVersionUtil.sanitize(fetchResponse.getHeaders().get(HEADER_KEY_X_NOTES_API_VERSIONS)); + localAccount.setApiVersion(newApiVersion); + repo.updateApiVersion(localAccount.getId(), newApiVersion); + Log.d(TAG, "ApiVersion: " + newApiVersion); return true; } catch (Throwable t) { final Throwable cause = t.getCause(); @@ -274,6 +280,8 @@ abstract class NotesServerSyncTask extends Thread { Log.d(TAG, "Server returned HTTP Status Code " + httpException.getStatusCode() + " - Server is in maintenance mode."); return true; } + } else if (cause.getClass() == NextcloudApiNotRespondingException.class || cause instanceof NextcloudApiNotRespondingException) { + apiProvider.invalidateAPICache(ssoAccount); } } exceptions.add(t); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java index 1d4a8bc7..adb7eff0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncWorker.java @@ -15,7 +15,6 @@ import androidx.work.WorkerParameters; import java.util.Objects; import java.util.concurrent.TimeUnit; -import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.persistence.entity.Account; public class SyncWorker extends Worker { @@ -44,22 +43,20 @@ public class SyncWorker extends Worker { return Result.success(); } - public static void update(@NonNull Context context, @NonNull String preferenceValue) { + /** + * Set up sync work to enabled every 15 minutes or just disabled + * https://github.com/stefan-niedermann/nextcloud-notes/issues/1168 + * @param context the application + * @param backgroundSync the toggle result backgroundSync + */ + + public static void update(@NonNull Context context, boolean backgroundSync) { deregister(context); - if (!context.getString(R.string.pref_value_sync_off).equals(preferenceValue)) { - int repeatInterval = 15; - TimeUnit unit = TimeUnit.MINUTES; - if (context.getString(R.string.pref_value_sync_1_hour).equals(preferenceValue)) { - repeatInterval = 1; - unit = TimeUnit.HOURS; - } else if (context.getString(R.string.pref_value_sync_6_hours).equals(preferenceValue)) { - repeatInterval = 6; - unit = TimeUnit.HOURS; - } - PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(SyncWorker.class, repeatInterval, unit) + if (backgroundSync) { + PeriodicWorkRequest work = new PeriodicWorkRequest.Builder(SyncWorker.class, 15, TimeUnit.MINUTES) .setConstraints(constraints).build(); WorkManager.getInstance(context.getApplicationContext()).enqueueUniquePeriodicWork(WORKER_TAG, ExistingPeriodicWorkPolicy.REPLACE, work); - Log.i(TAG, "Registering worker running each " + repeatInterval + " " + unit); + Log.i(TAG, "Registering worker running each " + 15 + " " + TimeUnit.MINUTES); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java index 085c0a16..7723e1f0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/AccountDao.java @@ -1,6 +1,7 @@ package it.niedermann.owncloud.notes.persistence.dao; import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Delete; @@ -18,10 +19,10 @@ public interface AccountDao { long insert(Account localAccount); @Delete - int deleteAccount(Account localAccount); + void deleteAccount(Account localAccount); - String getAccounts = "SELECT * FROM Account"; - String getAccountById = "SELECT * FROM Account WHERE ID = :accountId"; + String getAccounts = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account"; + String getAccountById = "SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ID = :accountId"; @Query(getAccounts) LiveData<List<Account>> getAccounts$(); @@ -35,7 +36,7 @@ public interface AccountDao { @Query(getAccountById) Account getAccountById(long accountId); - @Query("SELECT * FROM Account WHERE ACCOUNTNAME = :accountName") + @Query("SELECT id, url, userName, accountName, eTag, modified, apiVersion, color, textColor, capabilitiesEtag, COALESCE(displayName, userName) as displayName FROM Account WHERE ACCOUNTNAME = :accountName") Account getAccountByName(String accountName); @Query("SELECT COUNT(*) FROM Account") @@ -55,4 +56,7 @@ public interface AccountDao { @Query("UPDATE Account SET APIVERSION = :apiVersion WHERE id = :id AND ((APIVERSION IS NULL AND :apiVersion IS NOT NULL) OR (APIVERSION IS NOT NULL AND :apiVersion IS NULL) OR APIVERSION <> :apiVersion)") int updateApiVersion(Long id, String apiVersion); + + @Query("UPDATE Account SET DISPLAYNAME = :displayName WHERE id = :id") + void updateDisplayName(long id, @Nullable String displayName); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java index 618dff37..ca111727 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java @@ -29,11 +29,7 @@ public interface NoteDao { @Update(onConflict = OnConflictStrategy.REPLACE) int updateNote(Note newNote); - @Query("DELETE FROM NOTE WHERE accountId = :accountId") - int deleteByAccountId(Long accountId); - String getNoteById = "SELECT * FROM NOTE WHERE id = :id"; - String getContent = "SELECT content FROM NOTE WHERE id = :id"; String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId"; String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1"; String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC"; @@ -51,6 +47,9 @@ public interface NoteDao { @Query(getNoteById) Note getNoteById(long id); + @Query("SELECT remoteId FROM NOTE WHERE id = :id") + Long getRemoteId(long id); + @Query(count) LiveData<Integer> count$(long accountId); @@ -63,12 +62,6 @@ public interface NoteDao { @Query(countFavorites) Integer countFavorites(long accountId); - @Query(getContent) - LiveData<String> getContent$(Long id); - - @Query(getContent) - String getContent(Long id); - @Query(searchRecentByModified) LiveData<List<Note>> searchRecentByModified$(long accountId, String query); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java index 09f3fc26..016b2fd0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Account.java @@ -58,43 +58,21 @@ public class Account implements Serializable { private int textColor = Color.WHITE; @Nullable private String capabilitiesETag; + @Nullable + private String displayName; public Account() { // Default constructor } - public Account(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities) { + public Account(@NonNull String url, @NonNull String username, @NonNull String accountName, @Nullable String displayName, @NonNull Capabilities capabilities) { setUrl(url); setUserName(username); setAccountName(accountName); + setDisplayName(displayName); setCapabilities(capabilities); } - @Nullable - public ApiVersion getPreferredApiVersion() { - // TODO move this logic to NotesClient? - try { - if (apiVersion == null) { - return null; - } - final JSONArray versionsArray = new JSONArray(apiVersion); - final Collection<ApiVersion> supportedApiVersions = new HashSet<>(versionsArray.length()); - for (int i = 0; i < versionsArray.length(); i++) { - final ApiVersion parsedApiVersion = ApiVersion.of(versionsArray.getString(i)); - for (ApiVersion temp : ApiVersion.SUPPORTED_API_VERSIONS) { - if (temp.equals(parsedApiVersion)) { - supportedApiVersions.add(parsedApiVersion); - break; - } - } - } - return Collections.max(supportedApiVersions); - } catch (JSONException | NoSuchElementException e) { - e.printStackTrace(); - return null; - } - } - public void setCapabilities(@NonNull Capabilities capabilities) { capabilitiesETag = capabilities.getETag(); apiVersion = capabilities.getApiVersion(); @@ -189,6 +167,15 @@ public class Account implements Serializable { this.capabilitiesETag = capabilitiesETag; } + @Nullable + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(@Nullable String displayName) { + this.displayName = displayName; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -227,6 +214,7 @@ public class Account implements Serializable { return result; } + @NonNull @Override public String toString() { return "Account{" + diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java index 376c099d..e0d0325c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/Note.java @@ -255,6 +255,7 @@ public class Note implements Serializable, Item { return result; } + @NonNull @Override public String toString() { return "Note{" + diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java index 6938e41c..a66fc0e9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_14_15.java @@ -12,10 +12,10 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import java.util.Hashtable; -import it.niedermann.owncloud.notes.shared.util.DatabaseIndexUtil; - public class Migration_14_15 extends Migration { + private static final String TAG = Migration_14_15.class.getSimpleName(); + public Migration_14_15() { super(14, 15); } @@ -43,14 +43,14 @@ public class Migration_14_15 extends Migration { "EXCERPT TEXT NOT NULL DEFAULT '', " + "FOREIGN KEY(CATEGORY) REFERENCES CATEGORIES(CATEGORY_ID), " + "FOREIGN KEY(ACCOUNT_ID) REFERENCES ACCOUNTS(ID))"); - DatabaseIndexUtil.createIndex(db, "NOTES", "REMOTEID", "ACCOUNT_ID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED"); + createIndex(db, "NOTES", "REMOTEID", "ACCOUNT_ID", "STATUS", "FAVORITE", "CATEGORY", "MODIFIED"); db.execSQL("CREATE TABLE CATEGORIES(" + "CATEGORY_ID INTEGER PRIMARY KEY AUTOINCREMENT, " + "CATEGORY_ACCOUNT_ID INTEGER NOT NULL, " + "CATEGORY_TITLE TEXT NOT NULL, " + "UNIQUE( CATEGORY_ACCOUNT_ID , CATEGORY_TITLE), " + "FOREIGN KEY(CATEGORY_ACCOUNT_ID) REFERENCES ACCOUNTS(ID))"); - DatabaseIndexUtil.createIndex(db, "CATEGORIES", "CATEGORY_ID", "CATEGORY_ACCOUNT_ID", "CATEGORY_TITLE"); + createIndex(db, "CATEGORIES", "CATEGORY_ID", "CATEGORY_ACCOUNT_ID", "CATEGORY_TITLE"); // A hashtable storing categoryTitle - categoryId Mapping // This is used to prevent too many searches in database Hashtable<String, Integer> categoryTitleIdMap = new Hashtable<>(); @@ -91,4 +91,16 @@ public class Migration_14_15 extends Migration { tmpNotesCursor.close(); db.execSQL("DROP TABLE IF EXISTS " + tmpTableNotes); } + + private static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String... columns) { + for (String column : columns) { + createIndex(db, table, column); + } + } + + private static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String column) { + String indexName = table + "_" + column + "_idx"; + Log.v(TAG, "Creating database index: CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); + db.execSQL("CREATE INDEX " + indexName + " ON " + table + "(" + column + ")"); + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java index 343f9e81..576e2204 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_18_19.java @@ -9,9 +9,13 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import com.bumptech.glide.Glide; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + public class Migration_18_19 extends Migration { private static final String TAG = Migration_18_19.class.getSimpleName(); + private final ExecutorService executor = Executors.newSingleThreadExecutor(); @NonNull private final Context context; @@ -27,9 +31,9 @@ public class Migration_18_19 extends Migration { */ @Override public void migrate(@NonNull SupportSQLiteDatabase db) { - new Thread(() -> { + executor.submit(() -> { Log.i(TAG, "Clearing Glide disk cache"); Glide.get(context.getApplicationContext()).clearDiskCache(); - }).start(); + }); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java new file mode 100644 index 00000000..f4413bba --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_21_22.java @@ -0,0 +1,43 @@ +package it.niedermann.owncloud.notes.persistence.migration; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import it.niedermann.owncloud.notes.persistence.SyncWorker; + +/** + * Enabling backgroundSync, set from {@link String} values to {@link Boolean} values + * https://github.com/stefan-niedermann/nextcloud-notes/issues/1168 + */ +public class Migration_21_22 extends Migration { + @NonNull + private final Context context; + + public Migration_21_22(@NonNull Context context) { + super(21, 22); + this.context = context; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = sharedPreferences.edit(); + if (sharedPreferences.contains("backgroundSync")) { + editor.remove("backgroundSync"); + if (sharedPreferences.getString("backgroundSync", "").equals("off")) { + editor.putBoolean("backgroundSync", false); + } else { + editor.putBoolean("backgroundSync", true); + SyncWorker.update(context, true); + } + } else { + SyncWorker.update(context, true); + } + editor.apply(); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java new file mode 100644 index 00000000..b6a7494b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/migration/Migration_22_23.java @@ -0,0 +1,101 @@ +package it.niedermann.owncloud.notes.persistence.migration; + +import android.content.ContentValues; +import android.database.Cursor; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.OnConflictStrategy; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +import it.niedermann.owncloud.notes.persistence.ApiProvider; +import it.niedermann.owncloud.notes.persistence.entity.Account; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; + +/** + * Add <code>displayName</code> property to {@link Account}. + * <p> + * See: <a href="https://github.com/stefan-niedermann/nextcloud-notes/issues/1079">#1079 Show DisplayName instead of uid attribute for LDAP users</a> + * <p> + * Sanitizes the stored API versions in the database. + */ +public class Migration_22_23 extends Migration { + + public Migration_22_23() { + super(22, 23); + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase db) { + addDisplayNameToAccounts(db); + sanitizeAccounts(db); + } + + private static void addDisplayNameToAccounts(@NonNull SupportSQLiteDatabase db) { + db.execSQL("ALTER TABLE Account ADD COLUMN displayName TEXT"); + } + + private static void sanitizeAccounts(@NonNull SupportSQLiteDatabase db) { + final Cursor cursor = db.query("SELECT id, apiVersion FROM ACCOUNT", null); + final ContentValues values = new ContentValues(1); + + final int COLUMN_POSITION_ID = cursor.getColumnIndex("id"); + final int COLUMN_POSITION_API_VERSION = cursor.getColumnIndex("apiVersion"); + + while (cursor.moveToNext()) { + values.put("APIVERSION", sanitizeApiVersion(cursor.getString(COLUMN_POSITION_API_VERSION))); + db.update("ACCOUNT", OnConflictStrategy.REPLACE, values, "ID = ?", new String[]{String.valueOf(cursor.getLong(COLUMN_POSITION_ID))}); + } + cursor.close(); + ApiProvider.getInstance().invalidateAPICache(); + } + + @Nullable + public static String sanitizeApiVersion(@Nullable String raw) { + if (TextUtils.isEmpty(raw)) { + return null; + } + + JSONArray a; + try { + a = new JSONArray(raw); + } catch (JSONException e) { + try { + a = new JSONArray("[" + raw + "]"); + } catch (JSONException e1) { + return null; + } + } + + final Collection<ApiVersion> result = new ArrayList<>(); + for (int i = 0; i < a.length(); i++) { + try { + final ApiVersion version = ApiVersion.of(a.getString(i)); + if (version.getMajor() != 0 || version.getMinor() != 0) { + result.add(version); + } + } catch (Exception ignored) { + } + } + if (result.isEmpty()) { + return null; + } + return "[" + + result + .stream() + .filter(Objects::nonNull) + .map(v -> v.getMajor() + "." + v.getMinor()) + .collect(Collectors.joining(",")) + + "]"; + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java index c9bf78da..141443e3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java @@ -1,77 +1,63 @@ package it.niedermann.owncloud.notes.persistence.sync; import android.graphics.Color; -import android.util.Log; -import com.bumptech.glide.load.HttpException; +import androidx.annotation.ColorInt; + import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.lang.reflect.Type; import it.niedermann.android.util.ColorUtil; import it.niedermann.owncloud.notes.shared.model.Capabilities; -import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; - +/** + * Deserialization of <code><a href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html?highlight=ocs#theming-capabilities">OcsCapabilities</a></code> to {@link Capabilities} is more complex than just mapping the JSON values to the Pojo properties. + * + * <ul> + * <li>The supported API versions of the Notes app are checked and <code>null</code>ed in case they are not present to maintain backward compatibility</li> + * <li>The color hex codes of the theming app are sanitized and mapped to {@link ColorInt}s</li> + * </ul> + */ public class CapabilitiesDeserializer implements JsonDeserializer<Capabilities> { - private static final String TAG = CapabilitiesDeserializer.class.getSimpleName(); - - private static final String JSON_OCS = "ocs"; - private static final String JSON_OCS_META = "meta"; - private static final String JSON_OCS_META_STATUSCODE = "statuscode"; - private static final String JSON_OCS_DATA = "data"; - private static final String JSON_OCS_DATA_CAPABILITIES = "capabilities"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES = "notes"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION = "api_version"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING = "theming"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR = "color"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text"; + private static final String CAPABILITIES = "capabilities"; + private static final String CAPABILITIES_NOTES = "notes"; + private static final String CAPABILITIES_NOTES_API_VERSION = "api_version"; + private static final String CAPABILITIES_THEMING = "theming"; + private static final String CAPABILITIES_THEMING_COLOR = "color"; + private static final String CAPABILITIES_THEMING_COLOR_TEXT = "color-text"; @Override public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { final Capabilities response = new Capabilities(); - final JsonObject ocs = json.getAsJsonObject().getAsJsonObject(JSON_OCS); - if (ocs.has(JSON_OCS_META)) { - final JsonObject meta = ocs.getAsJsonObject(JSON_OCS_META); - if (meta.has(JSON_OCS_META_STATUSCODE)) { - if (meta.get(JSON_OCS_META_STATUSCODE).getAsInt() == HTTP_UNAVAILABLE) { - Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode."); - throw new JsonParseException(new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE))); + final JsonObject data = json.getAsJsonObject(); + if (data.has(CAPABILITIES)) { + final JsonObject capabilities = data.getAsJsonObject(CAPABILITIES); + if (capabilities.has(CAPABILITIES_NOTES)) { + final JsonObject notes = capabilities.getAsJsonObject(CAPABILITIES_NOTES); + if (notes.has(CAPABILITIES_NOTES_API_VERSION)) { + response.setApiVersion(notes.get(CAPABILITIES_NOTES_API_VERSION).toString()); } } - } - if (ocs.has(JSON_OCS_DATA)) { - final JsonObject data = ocs.getAsJsonObject(JSON_OCS_DATA); - if (data.has(JSON_OCS_DATA_CAPABILITIES)) { - final JsonObject capabilities = data.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES); - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) { - final JsonObject notes = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_NOTES); - if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) { - final JsonElement apiVersion = notes.get(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION); - response.setApiVersion(apiVersion.isJsonArray() ? apiVersion.toString() : null); + if (capabilities.has(CAPABILITIES_THEMING)) { + final JsonObject theming = capabilities.getAsJsonObject(CAPABILITIES_THEMING); + if (theming.has(CAPABILITIES_THEMING_COLOR)) { + try { + response.setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(CAPABILITIES_THEMING_COLOR).getAsString()))); + } catch (Exception e) { + e.printStackTrace(); } } - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) { - final JsonObject theming = capabilities.getAsJsonObject(JSON_OCS_DATA_CAPABILITIES_THEMING); - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) { - try { - response.setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR).getAsString()))); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) { - try { - response.setTextColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT).getAsString()))); - } catch (Exception e) { - e.printStackTrace(); - } + if (theming.has(CAPABILITIES_THEMING_COLOR_TEXT)) { + try { + response.setTextColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.get(CAPABILITIES_THEMING_COLOR_TEXT).getAsString()))); + } catch (Exception e) { + e.printStackTrace(); } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java index 4ab8371e..3e552ae6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/NotesAPI.java @@ -80,10 +80,14 @@ public class NotesAPI { } public Call<Note> editNote(@NonNull Note note) { + final Long remoteId = note.getRemoteId(); + if (remoteId == null) { + throw new IllegalArgumentException("remoteId of a " + Note.class.getSimpleName() + " must not be null if this object is used for editing a remote note."); + } if (ApiVersion.API_VERSION_1_0.equals(usedApiVersion)) { - return notesAPI_1_0.editNote(note, note.getRemoteId()); + return notesAPI_1_0.editNote(note, remoteId); } else if (ApiVersion.API_VERSION_0_2.equals(usedApiVersion)) { - return notesAPI_0_2.editNote(new Note_0_2(note), note.getRemoteId()); + return notesAPI_0_2.editNote(new Note_0_2(note), remoteId); } else { throw new UnsupportedOperationException("Used API version " + usedApiVersion + " does not support editNote()."); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java index 27ef57c4..e24ef7ef 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java @@ -5,8 +5,12 @@ import com.nextcloud.android.sso.api.ParsedResponse; import io.reactivex.Observable; import it.niedermann.owncloud.notes.shared.model.Capabilities; +import it.niedermann.owncloud.notes.shared.model.OcsResponse; +import it.niedermann.owncloud.notes.shared.model.OcsUser; +import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Path; /** * @link <a href="https://deck.readthedocs.io/en/latest/API/">Deck REST API</a> @@ -14,5 +18,8 @@ import retrofit2.http.Header; public interface OcsAPI { @GET("capabilities?format=json") - Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag); + Observable<ParsedResponse<OcsResponse<Capabilities>>> getCapabilities(@Header("If-None-Match") String eTag); + + @GET("users/{userId}?format=json") + Call<OcsResponse<OcsUser>> getUser(@Path("userId") String userId); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java index f872aac3..2dcd99ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesActivity.java @@ -3,28 +3,28 @@ package it.niedermann.owncloud.notes.preferences; import android.os.Bundle; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; import it.niedermann.owncloud.notes.LockedActivity; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.ActivityPreferencesBinding; -/** - * Allows to change application settings. - */ - public class PreferencesActivity extends LockedActivity { + private PreferencesViewModel viewModel; private ActivityPreferencesBinding binding; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(this).get(PreferencesViewModel.class); + viewModel.resultCode$.observe(this, this::setResult); + binding = ActivityPreferencesBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); - setResult(RESULT_CANCELED); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container_view, new PreferencesFragment()) .commit(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java index 8af467fb..a38fd56c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesFragment.java @@ -8,6 +8,7 @@ import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; +import androidx.lifecycle.ViewModelProvider; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; @@ -20,27 +21,25 @@ import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.persistence.SyncWorker; import it.niedermann.owncloud.notes.shared.util.DeviceCredentialUtil; -import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets; - public class PreferencesFragment extends PreferenceFragmentCompat implements Branded { private static final String TAG = PreferencesFragment.class.getSimpleName(); + private PreferencesViewModel viewModel; + private BrandedSwitchPreference fontPref; private BrandedSwitchPreference lockPref; private BrandedSwitchPreference wifiOnlyPref; private BrandedSwitchPreference gridViewPref; private BrandedSwitchPreference preventScreenCapturePref; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } + private BrandedSwitchPreference backgroundSyncPref; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.preferences); + viewModel = new ViewModelProvider(requireActivity()).get(PreferencesViewModel.class); + fontPref = findPreference(getString(R.string.pref_key_font)); gridViewPref = findPreference(getString(R.string.pref_key_gridview)); @@ -48,7 +47,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra gridViewPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { final Boolean gridView = (Boolean) newValue; Log.v(TAG, "gridView: " + gridView); - requireActivity().setResult(Activity.RESULT_OK); + viewModel.resultCode$.setValue(Activity.RESULT_OK); NotesApplication.updateGridViewEnabled(gridView); return true; }); @@ -79,7 +78,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra assert themePref != null; themePref.setOnPreferenceChangeListener((preference, newValue) -> { NotesApplication.setAppTheme(DarkModeSetting.valueOf((String) newValue)); - requireActivity().setResult(Activity.RESULT_OK); + viewModel.resultCode$.setValue(Activity.RESULT_OK); ActivityCompat.recreate(requireActivity()); return true; }); @@ -91,11 +90,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra return true; }); - final ListPreference syncPref = findPreference(getString(R.string.pref_key_background_sync)); - assert syncPref != null; - syncPref.setOnPreferenceChangeListener((preference, newValue) -> { - Log.i(TAG, "syncPref: " + preference + " - newValue: " + newValue); - SyncWorker.update(requireContext(), newValue.toString()); + backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync)); + assert backgroundSyncPref != null; + backgroundSyncPref.setOnPreferenceChangeListener((preference, newValue) -> { + Log.i(TAG, "backgroundSync: " + newValue); + SyncWorker.update(requireContext(), (Boolean) newValue); return true; }); } @@ -112,6 +111,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra } } + /** + * Change color for backgroundSyncPref as well + * https://github.com/stefan-niedermann/nextcloud-deck/issues/531 + * + * @param mainColor color of main brand + * @param textColor color of text + */ + @Override public void applyBrand(int mainColor, int textColor) { fontPref.applyBrand(mainColor, textColor); @@ -119,5 +126,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Bra wifiOnlyPref.applyBrand(mainColor, textColor); gridViewPref.applyBrand(mainColor, textColor); preventScreenCapturePref.applyBrand(mainColor, textColor); + backgroundSyncPref.applyBrand(mainColor, textColor); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java new file mode 100644 index 00000000..dfde6c92 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/preferences/PreferencesViewModel.java @@ -0,0 +1,9 @@ +package it.niedermann.owncloud.notes.preferences; + +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class PreferencesViewModel extends ViewModel { + + public final MutableLiveData<Integer> resultCode$ = new MutableLiveData<>(); +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java index 7688b1b1..5d3d2963 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/account/AccountChooserViewHolder.java @@ -31,7 +31,7 @@ public class AccountChooserViewHolder extends RecyclerView.ViewHolder { .into(binding.accountItemAvatar); binding.accountLayout.setOnClickListener((v) -> targetAccountConsumer.accept(localAccount)); - binding.accountName.setText(localAccount.getUserName()); + binding.accountName.setText(localAccount.getDisplayName()); binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost()); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java index ee2fdc3a..589f2571 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/ApiVersion.java @@ -42,10 +42,6 @@ public class ApiVersion implements Comparable<ApiVersion> { return minor; } - public String getOriginalVersion() { - return originalVersion; - } - public static ApiVersion of(String versionString) { int major = 0, minor = 0; if (versionString != null) { @@ -90,6 +86,9 @@ public class ApiVersion implements Comparable<ApiVersion> { // return getMajor() >= 1 && getMinor() >= 2; } + /** + * Checks only the <strong>{@link #major}</strong> version. + */ @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java index 5514a91b..06bd867d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java @@ -1,103 +1,21 @@ package it.niedermann.owncloud.notes.shared.model; import android.graphics.Color; -import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.bumptech.glide.load.HttpException; -import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; - -import org.json.JSONException; -import org.json.JSONObject; - -import it.niedermann.android.util.ColorUtil; - -import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; - -/** - * This entity class is used to return relevant data of the HTTP response. - */ public class Capabilities { - private static final String TAG = Capabilities.class.getSimpleName(); - - private static final String JSON_OCS = "ocs"; - private static final String JSON_OCS_META = "meta"; - private static final String JSON_OCS_META_STATUSCODE = "statuscode"; - private static final String JSON_OCS_DATA = "data"; - private static final String JSON_OCS_DATA_CAPABILITIES = "capabilities"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES = "notes"; - private static final String JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION = "api_version"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING = "theming"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR = "color"; - private static final String JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT = "color-text"; - private String apiVersion = null; - @ColorInt - private int color = -16743735; + private int color = -16743735; // #0082C9 @ColorInt - private int textColor = -16777216; + private int textColor = Color.WHITE; @Nullable private String eTag; - public Capabilities() { - - } - - @VisibleForTesting - public Capabilities(@NonNull String response, @Nullable String eTag) throws NextcloudHttpRequestFailedException { - this.eTag = eTag; - final JSONObject ocs; - try { - ocs = new JSONObject(response).getJSONObject(JSON_OCS); - if (ocs.has(JSON_OCS_META)) { - final JSONObject meta = ocs.getJSONObject(JSON_OCS_META); - if (meta.has(JSON_OCS_META_STATUSCODE)) { - if (meta.getInt(JSON_OCS_META_STATUSCODE) == HTTP_UNAVAILABLE) { - Log.i(TAG, "Capabilities Endpoint: This instance is currently in maintenance mode."); - throw new NextcloudHttpRequestFailedException(HTTP_UNAVAILABLE, new HttpException(HTTP_UNAVAILABLE)); - } - } - } - if (ocs.has(JSON_OCS_DATA)) { - final JSONObject data = ocs.getJSONObject(JSON_OCS_DATA); - if (data.has(JSON_OCS_DATA_CAPABILITIES)) { - final JSONObject capabilities = data.getJSONObject(JSON_OCS_DATA_CAPABILITIES); - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_NOTES)) { - final JSONObject notes = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_NOTES); - if (notes.has(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION)) { - this.apiVersion = notes.getString(JSON_OCS_DATA_CAPABILITIES_NOTES_API_VERSION); - } - } - if (capabilities.has(JSON_OCS_DATA_CAPABILITIES_THEMING)) { - final JSONObject theming = capabilities.getJSONObject(JSON_OCS_DATA_CAPABILITIES_THEMING); - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR)) { - try { - this.color = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR))); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (theming.has(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT)) { - try { - this.textColor = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(theming.getString(JSON_OCS_DATA_CAPABILITIES_THEMING_COLOR_TEXT))); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - public void setApiVersion(String apiVersion) { this.apiVersion = apiVersion; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java index 94bcda38..6a36ade1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/CategorySortingMethod.java @@ -7,7 +7,7 @@ public enum CategorySortingMethod { private final int id; private final String title; // sorting method OrderBy for SQL - /*** + /** * Constructor * @param title given sorting method OrderBy */ @@ -16,7 +16,7 @@ public enum CategorySortingMethod { this.title = title; } - /*** + /** * Retrieve the sorting method id represented in database * @return the sorting method id for the enum item */ @@ -24,7 +24,7 @@ public enum CategorySortingMethod { return this.id; } - /*** + /** * Retrieve the sorting method order for SQL * @return the sorting method order for the enum item */ @@ -32,7 +32,7 @@ public enum CategorySortingMethod { return this.title; } - /*** + /** * Retrieve the corresponding enum value with given the index (ordinal) * @param id the id of the corresponding enum value stored in DB * @return the corresponding enum item with the index (ordinal) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java new file mode 100644 index 00000000..0fea9a92 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsResponse.java @@ -0,0 +1,30 @@ +package it.niedermann.owncloud.notes.shared.model; + +import com.google.gson.annotations.Expose; + +/** + * <a href="https://www.open-collaboration-services.org/">OpenCollaborationServices</a> + * + * @param <T> defines the payload of this {@link OcsResponse}. + */ +public class OcsResponse<T> { + + @Expose + public OcsWrapper<T> ocs; + + public static class OcsWrapper<T> { + @Expose + public OcsMeta meta; + @Expose + public T data; + } + + public static class OcsMeta { + @Expose + public String status; + @Expose + public int statuscode; + @Expose + public String message; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java new file mode 100644 index 00000000..9248abdf --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/OcsUser.java @@ -0,0 +1,16 @@ +package it.niedermann.owncloud.notes.shared.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Equivalent of an <code><a href="https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-api-overview.html?highlight=ocs#user-metadata">OcsUser</a></code> + */ +public class OcsUser { + @Expose + @SerializedName("id") + public String userId; + @Expose + @SerializedName("displayname") + public String displayName; +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java new file mode 100644 index 00000000..57788472 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ApiVersionUtil.java @@ -0,0 +1,105 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; + +import it.niedermann.owncloud.notes.shared.model.ApiVersion; + +public class ApiVersionUtil { + + private ApiVersionUtil() { + throw new UnsupportedOperationException("Do not instantiate this util class."); + } + + /** + * @return a {@link Collection} of all valid {@link ApiVersion}s which have been found in {@param raw}. + */ + @NonNull + public static Collection<ApiVersion> parse(@Nullable String raw) { + if (TextUtils.isEmpty(raw)) { + return Collections.emptyList(); + } + + JSONArray a; + try { + a = new JSONArray(raw); + } catch (JSONException e) { + try { + a = new JSONArray("[" + raw + "]"); + } catch (JSONException e1) { + return Collections.emptyList(); + } + } + + final Collection<ApiVersion> result = new ArrayList<>(); + for (int i = 0; i < a.length(); i++) { + try { + final ApiVersion version = ApiVersion.of(a.getString(i)); + if (version.getMajor() != 0 || version.getMinor() != 0) { + result.add(version); + } + } catch (Exception ignored) { + } + } + return result; + } + + /** + * @return a serialized {@link String} of the given {@param apiVersions} or <code>null</code>. + */ + @Nullable + public static String serialize(@Nullable Collection<ApiVersion> apiVersions) { + if (apiVersions == null || apiVersions.isEmpty()) { + return null; + } + return "[" + + apiVersions + .stream() + .filter(Objects::nonNull) + .map(v -> v.getMajor() + "." + v.getMinor()) + .collect(Collectors.joining(",")) + + "]"; + } + + @Nullable + public static String sanitize(@Nullable String raw) { + return serialize(parse(raw)); + } + + /** + * @return the highest {@link ApiVersion} that is supported by the server according to {@param raw}, + * whose major version is also supported by this app (see {@link ApiVersion#SUPPORTED_API_VERSIONS}). + * Returns <code>null</code> if no better version could be found. + */ + @Nullable + public static ApiVersion getPreferredApiVersion(@Nullable String raw) { + return parse(raw) + .stream() + .filter(version -> Arrays.asList(ApiVersion.SUPPORTED_API_VERSIONS).contains(version)) + .max((o1, o2) -> { + if (o2.getMajor() > o1.getMajor()) { + return -1; + } else if (o2.getMajor() < o1.getMajor()) { + return 1; + } else if (o2.getMinor() > o1.getMinor()) { + return -1; + } else if (o2.getMinor() < o1.getMinor()) { + return 1; + } + return 0; + }) + .orElse(null); + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java index 03eb1097..35625119 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/CustomAppGlideModule.java @@ -1,18 +1,37 @@ package it.niedermann.owncloud.notes.shared.util; import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.UiThread; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + @GlideModule public class CustomAppGlideModule extends AppGlideModule { + + private static final String TAG = CustomAppGlideModule.class.getSimpleName(); + private static final ExecutorService clearDiskCacheExecutor = Executors.newSingleThreadExecutor(); + @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { super.registerComponents(context, glide, registry); } + + @UiThread + public static void clearCache(@NonNull Context context) { + Log.i(TAG, "Clearing Glide memory cache"); + Glide.get(context).clearMemory(); + clearDiskCacheExecutor.submit(() -> { + Log.i(TAG, "Clearing Glide disk cache"); + Glide.get(context.getApplicationContext()).clearDiskCache(); + }); + } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java deleted file mode 100644 index 8506c713..00000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DatabaseIndexUtil.java +++ /dev/null @@ -1,41 +0,0 @@ -package it.niedermann.owncloud.notes.shared.util; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteDatabase; - -public class DatabaseIndexUtil { - - private static final String TAG = DatabaseIndexUtil.class.getSimpleName(); - - private DatabaseIndexUtil() { - - } - - public static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String... columns) { - for (String column : columns) { - createIndex(db, table, column); - } - } - - public static void createIndex(@NonNull SupportSQLiteDatabase db, @NonNull String table, @NonNull String column) { - String indexName = table + "_" + column + "_idx"; - Log.v(TAG, "Creating database index: CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")"); - db.execSQL("CREATE INDEX " + indexName + " ON " + table + "(" + column + ")"); - } - - public static void dropIndexes(@NonNull SupportSQLiteDatabase db) { - try (Cursor c = db.query("SELECT name, sql FROM sqlite_master WHERE type = 'index'")) { - while (c.moveToNext()) { - // Skip automatic indexes which we can't drop manually - if (c.getString(1) != null) { - Log.v(TAG, "Deleting database index: DROP INDEX " + c.getString(0)); - db.execSQL("DROP INDEX " + c.getString(0)); - } - } - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java index 034eea5a..14163e91 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DeviceCredentialUtil.java @@ -12,7 +12,7 @@ public class DeviceCredentialUtil { private static final String TAG = DeviceCredentialUtil.class.getSimpleName(); private DeviceCredentialUtil() { - // utility class -> private constructor + throw new UnsupportedOperationException("Do not instantiate this util class."); } public static boolean areCredentialsAvailable(Context context) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index ad6b6793..06cbf57d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -2,27 +2,21 @@ package it.niedermann.owncloud.notes.shared.util; import android.content.Context; import android.content.res.Resources; -import android.graphics.Color; -import android.text.Spannable; -import android.text.TextPaint; -import android.text.TextUtils; -import android.text.style.MetricAffectingSpan; +import android.graphics.Rect; +import android.os.Build; +import android.util.TypedValue; +import android.view.View; +import android.view.WindowInsets; -import androidx.annotation.ColorInt; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.Collection; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import it.niedermann.android.util.ColorUtil; -import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter; import it.niedermann.owncloud.notes.main.navigation.NavigationItem; import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount; @@ -30,7 +24,7 @@ import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount; public class DisplayUtils { private DisplayUtils() { - + throw new UnsupportedOperationException("Do not instantiate this util class."); } public static List<NavigationItem.CategoryNavigationItem> convertToCategoryNavigationItem(@NonNull Context context, @NonNull Collection<CategoryWithNotesCount> counter) { @@ -52,4 +46,29 @@ public class DisplayUtils { } return new NavigationItem.CategoryNavigationItem("category:" + counter.getCategory(), counter.getCategory(), counter.getTotalNotes(), icon, counter.getAccountId(), counter.getCategory()); } + + /** + * Detect if the soft keyboard is open. + * On API prior to 30 we fall back to workaround which might be less reliable + * + * @param parentView View + * @return keyboardVisibility Boolean + */ + public static boolean isSoftKeyboardVisible(@NonNull View parentView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + final WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView); + if (insets != null) { + return insets.isVisible(WindowInsets.Type.ime()); + } + } + + //Arbitrary keyboard height + final int defaultKeyboardHeightDP = 100; + final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0); + final Rect rect = new Rect(); + final int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics()); + parentView.getWindowVisibleDisplayFrame(rect); + final int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top); + return heightDiff >= estimatedKeyboardHeight; + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java index e5b8afae..3d5a118c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java @@ -22,7 +22,7 @@ public class NoteUtil { public static final String EXCERPT_LINE_SEPARATOR = " "; private NoteUtil() { - + throw new UnsupportedOperationException("Do not instantiate this util class."); } /** @@ -117,7 +117,7 @@ public class NoteUtil { line = removeMarkdown(lines[currentLine]); } } else { - line = content; + line = removeMarkdown(content); } return line; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java index a445208b..42aaf79d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NotesColorUtil.java @@ -14,6 +14,7 @@ public final class NotesColorUtil { private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); private NotesColorUtil() { + throw new UnsupportedOperationException("Do not instantiate this util class."); } public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java index da529136..1e2542b0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SSOUtil.java @@ -19,7 +19,7 @@ public class SSOUtil { private static final String TAG = SSOUtil.class.getSimpleName(); private SSOUtil() { - + throw new UnsupportedOperationException("Do not instantiate this util class."); } /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java index 115d18dd..4b7aebd9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/ShareUtil.java @@ -14,6 +14,11 @@ import it.niedermann.android.markdown.MarkdownUtil; import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; public class ShareUtil { + + private ShareUtil() { + throw new UnsupportedOperationException("Do not instantiate this util class."); + } + public static void openShareDialog(@NonNull Context context, @Nullable String subject, @Nullable String text) { context.startActivity(Intent.createChooser(new Intent() .setAction(Intent.ACTION_SEND) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java index 8bf80cb9..27fec716 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SupportUtil.java @@ -13,7 +13,7 @@ import androidx.core.text.HtmlCompat; public class SupportUtil { private SupportUtil() { - + throw new UnsupportedOperationException("Do not instantiate this util class."); } /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java index 7cc84de3..8825ad98 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.java @@ -11,6 +11,8 @@ import android.util.Log; import android.widget.RemoteViews; import java.util.NoSuchElementException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.persistence.NotesRepository; @@ -18,6 +20,7 @@ import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData; public class NoteListWidget extends AppWidgetProvider { private static final String TAG = NoteListWidget.class.getSimpleName(); + private final ExecutorService executor = Executors.newCachedThreadPool(); static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { final NotesRepository repo = NotesRepository.getInstance(context); @@ -83,7 +86,7 @@ public class NoteListWidget extends AppWidgetProvider { final NotesRepository repo = NotesRepository.getInstance(context); for (int appWidgetId : appWidgetIds) { - new Thread(() -> repo.removeNoteListWidget(appWidgetId)).start(); + executor.submit(() -> repo.removeNoteListWidget(appWidgetId)); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java index a750ee1e..3d7122da 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetConfigurationActivity.java @@ -14,6 +14,9 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundExce import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import it.niedermann.owncloud.notes.LockedActivity; import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; @@ -34,6 +37,8 @@ import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType. public class NoteListWidgetConfigurationActivity extends LockedActivity { private static final String TAG = Activity.class.getSimpleName(); + private final ExecutorService executor = Executors.newCachedThreadPool(); + private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; private Account localAccount = null; @@ -104,7 +109,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { data.setAccountId(localAccount.getId()); data.setThemeMode(NotesApplication.getAppTheme(getApplicationContext()).getModeId()); - new Thread(() -> { + executor.submit(() -> { repo.createOrUpdateNoteListWidgetData(data); final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, getApplicationContext(), NoteListWidget.class) @@ -112,7 +117,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { setResult(RESULT_OK, updateIntent); getApplicationContext().sendBroadcast(updateIntent); finish(); - }).start(); + }); } public void onIconClick(NavigationItem item) { @@ -122,7 +127,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { binding.recyclerView.setAdapter(adapterCategories); - new Thread(() -> { + executor.submit(() -> { try { this.localAccount = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name); } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { @@ -133,7 +138,7 @@ public class NoteListWidgetConfigurationActivity extends LockedActivity { finish(); } runOnUiThread(() -> viewModel.getAdapterCategories(localAccount.getId()).observe(this, (navigationItems) -> adapterCategories.setItems(navigationItems))); - }).start(); + }); } @Override diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java index e208603a..106b9f93 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidget.java @@ -10,6 +10,9 @@ import android.net.Uri; import android.util.Log; import android.widget.RemoteViews; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.edit.BaseNoteFragment; import it.niedermann.owncloud.notes.edit.EditNoteActivity; @@ -19,6 +22,7 @@ import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData; public class SingleNoteWidget extends AppWidgetProvider { private static final String TAG = SingleNoteWidget.class.getSimpleName(); + private final ExecutorService executor = Executors.newCachedThreadPool(); static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) { final Intent templateIntent = new Intent(context, EditNoteActivity.class); @@ -69,7 +73,7 @@ public class SingleNoteWidget extends AppWidgetProvider { final NotesRepository repo = NotesRepository.getInstance(context); for (int appWidgetId : appWidgetIds) { - new Thread(() -> repo.removeSingleNoteWidget(appWidgetId)).start(); + executor.submit(() -> repo.removeSingleNoteWidget(appWidgetId)); } super.onDeleted(context, appWidgetIds); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java index eb06201a..a487e669 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/singlenote/SingleNoteWidgetConfigurationActivity.java @@ -52,7 +52,7 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity { int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - new Thread(() -> { + executor.submit(() -> { try { mainViewModel.createOrUpdateSingleNoteWidgetData( new SingleNoteWidgetData( @@ -71,6 +71,6 @@ public class SingleNoteWidgetConfigurationActivity extends MainActivity { } catch (SQLException e) { Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } - }).start(); + }); } } |