selection = adapter.getSelected();
- for (Integer i : selection) {
- DBNote note = (DBNote) adapter.getItem(i);
- db.deleteNoteAndSync(note.getId());
- // Not needed because of dbsync
- //adapter.remove(note);
- }
- mode.finish(); // Action picked, so close the CAB
- //after delete selection has to be cleared
- searchView.setIconified(true);
- refreshLists();
- return true;
- default:
- return false;
- }
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- adapter.clearSelection();
- mActionMode = null;
- adapter.notifyDataSetChanged();
- }
- }
-
-}
diff --git a/app/src/main/java/foundation/e/notes/android/activity/PreferencesActivity.java b/app/src/main/java/foundation/e/notes/android/activity/PreferencesActivity.java
deleted file mode 100644
index 0ebe351acb0fb371efb857003e1e1720ffe508a3..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/activity/PreferencesActivity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package foundation.e.notes.android.activity;
-
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.Window;
-import android.view.WindowManager;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-
-import foundation.e.notes.android.fragment.PreferencesFragment;
-import foundation.e.notes.util.ExceptionHandler;
-
-/**
- * Allows to change application settings.
- */
-
-public class PreferencesActivity extends AppCompatActivity {
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
- setResult(RESULT_CANCELED);
- getFragmentManager().beginTransaction()
- .replace(android.R.id.content, new PreferencesFragment())
- .commit();
-
- }
-
-}
diff --git a/app/src/main/java/foundation/e/notes/android/activity/SelectSingleNoteActivity.java b/app/src/main/java/foundation/e/notes/android/activity/SelectSingleNoteActivity.java
deleted file mode 100644
index a4e71a77b3b79e28ba88c9d578feaa469b60f04a..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/activity/SelectSingleNoteActivity.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package foundation.e.notes.android.activity;
-
-import android.app.Activity;
-import android.appwidget.AppWidgetManager;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.view.Menu;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-
-import androidx.annotation.ColorInt;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.android.appwidget.SingleNoteWidget;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.model.Item;
-import foundation.e.notes.model.ItemAdapter;
-import foundation.e.notes.util.Notes;
-import foundation.e.notes.util.ExceptionHandler;
-
-public class SelectSingleNoteActivity extends NotesListViewActivity {
-
- @BindView(R.id.fab_create)
- View fabCreate;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
- setResult(Activity.RESULT_CANCELED);
-
- SwipeRefreshLayout swipeRefreshLayout = getSwipeRefreshLayout();
-
- ButterKnife.bind(this);
- fabCreate.setVisibility(View.GONE);
-
- androidx.appcompat.app.ActionBar ab = getSupportActionBar();
- if (ab != null) {
- ab.setTitle(R.string.activity_select_single_note);
- }
- swipeRefreshLayout.setEnabled(false);
- swipeRefreshLayout.setRefreshing(false);
-
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- return true;
- }
-
- @Override
- public void onNoteClick(int position, View v) {
- ItemAdapter adapter = getItemAdapter();
- Item item = adapter.getItem(position);
- DBNote note = (DBNote) item;
- long noteID = note.getId();
- final Bundle extras = getIntent().getExtras();
-
- if (extras == null) {
- finish();
- }
-
- int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
- SharedPreferences.Editor sp = PreferenceManager.getDefaultSharedPreferences(this).edit();
-
- sp.putLong(SingleNoteWidget.WIDGET_KEY + appWidgetId, noteID);
- sp.apply();
-
- Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
- getApplicationContext(), SingleNoteWidget.class);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- setResult(RESULT_OK, updateIntent);
- getApplicationContext().sendBroadcast(updateIntent);
- finish();
- }
-
-}
diff --git a/app/src/main/java/foundation/e/notes/android/activity/SettingsActivity.java b/app/src/main/java/foundation/e/notes/android/activity/SettingsActivity.java
deleted file mode 100644
index dab548e04a5a271f680b3aae10fdaa27c82be938..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/activity/SettingsActivity.java
+++ /dev/null
@@ -1,554 +0,0 @@
-package foundation.e.notes.android.activity;
-
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.http.SslCertificate;
-import android.net.http.SslError;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.preference.PreferenceManager;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.webkit.SslErrorHandler;
-import android.webkit.WebSettings;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.cert4android.IOnCertificateDecision;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-import foundation.e.notes.persistence.NoteServerSyncHelper;
-import foundation.e.notes.util.ExceptionHandler;
-import foundation.e.notes.util.NotesClientUtil;
-import foundation.e.notes.util.NotesClientUtil.LoginStatus;
-
-import androidx.annotation.ColorInt;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
-
-import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.textfield.TextInputLayout;
-
-import java.io.ByteArrayInputStream;
-import java.net.URLDecoder;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-import static android.os.Process.killProcess;
-import static android.os.Process.myPid;
-
-/**
- * @author Nihar Thakkar
- *
- * Allows to set Settings like URL, Username and Password for Server-Synchronization
- * Created by stefan on 22.09.15.
- */
-public class SettingsActivity extends AppCompatActivity {
-
- public static final String SETTINGS_URL = "settingsUrl";
- public static final String SETTINGS_USERNAME = "settingsUsername";
- public static final String SETTINGS_PASSWORD = "settingsPassword";
- public static final String SETTINGS_KEY_ETAG = "notes_last_etag";
- public static final String SETTINGS_KEY_LAST_MODIFIED = "notes_last_modified";
- public static final String SETTINGS_IS_DEVICE_ACCOUNT = "is_device_account";
- public static final String DEFAULT_SETTINGS = "";
- public static final int CREDENTIALS_CHANGED = 3;
-
- public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
- public static final String WEBDAV_PATH_4_0_AND_LATER = "/remote.php/webdav";
-
- private SharedPreferences preferences = null;
-
- @BindView(R.id.settings_url)
- EditText field_url;
- @BindView(R.id.settings_username_wrapper)
- TextInputLayout username_wrapper;
- @BindView(R.id.settings_username)
- EditText field_username;
- @BindView(R.id.settings_password)
- EditText field_password;
- @BindView(R.id.settings_password_wrapper)
- TextInputLayout password_wrapper;
- @BindView(R.id.settings_submit)
- Button btn_submit;
- @BindView(R.id.settings_url_warn_http)
- View urlWarnHttp;
- private String old_password = "";
-
- private WebView webView;
-
- private boolean first_run = false;
- private boolean useWebLogin = true;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
- setContentView(R.layout.activity_settings);
- ButterKnife.bind(this);
-
- preferences = PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext());
-
- if (!NoteServerSyncHelper.isConfigured(this)) {
- first_run = true;
- if (getSupportActionBar() != null) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(false);
- }
- }
-
- setupListener();
-
- // Load current Preferences
- field_url.setText(preferences.getString(SETTINGS_URL, DEFAULT_SETTINGS));
- field_username.setText(preferences.getString(SETTINGS_USERNAME, DEFAULT_SETTINGS));
- old_password = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS);
-
- field_password.setOnEditorActionListener(new TextView.OnEditorActionListener() {
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- login();
- return true;
- }
- });
- field_password.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- setPasswordHint(hasFocus);
- }
- });
- setPasswordHint(false);
-
- handleSubmitButtonEnabled();
-
- }
-
- private void setupListener() {
- field_url.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString()));
- }
- });
- field_url.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- String url = NotesClientUtil.formatURL(field_url.getText().toString());
-
- if (NotesClientUtil.isHttp(url)) {
- urlWarnHttp.setVisibility(View.VISIBLE);
- } else {
- urlWarnHttp.setVisibility(View.GONE);
- }
-
- handleSubmitButtonEnabled();
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- }
- });
-
- field_username.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- handleSubmitButtonEnabled();
- }
-
- @Override
- public void afterTextChanged(Editable s) {
-
- }
- });
-
- btn_submit.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- login();
- }
- });
- }
-
- private void setPasswordHint(boolean hasFocus) {
- boolean unchangedHint = !hasFocus && field_password.getText().toString().isEmpty() && !old_password.isEmpty();
- password_wrapper.setHint(getString(unchangedHint ? R.string.settings_password_unchanged : R.string.settings_password));
- }
-
-
- @Override
- protected void onResume() {
- super.onResume();
-
- // Occurs in this scenario: User opens the app but doesn't configure the server settings, they then add the Create Note widget to home screen and configure
- // server settings there. The stale SettingsActivity is then displayed hence finish() here to close it down.
- if ((first_run) && (NoteServerSyncHelper.isConfigured(this))) {
- finish();
- }
- }
-
- /**
- * Prevent pressing back button on first run
- */
- @Override
- public void onBackPressed() {
- if (!first_run) {
- super.onBackPressed();
- }
- }
-
- private void legacyLogin() {
- String url = field_url.getText().toString().trim();
- String username = field_username.getText().toString();
- String password = field_password.getText().toString();
-
- if (password.isEmpty()) {
- password = old_password;
- }
-
- url = NotesClientUtil.formatURL(url);
-
- new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, username, password);
- }
-
- private void login() {
- if (useWebLogin) {
- webLogin();
- } else {
- legacyLogin();
- }
- }
-
- /**
- * Obtain the X509Certificate from SslError
- *
- * @param error SslError
- * @return X509Certificate from error
- */
- public static X509Certificate getX509CertificateFromError(SslError error) {
- Bundle bundle = SslCertificate.saveState(error.getCertificate());
- X509Certificate x509Certificate;
- byte[] bytes = bundle.getByteArray("x509-certificate");
- if (bytes == null) {
- x509Certificate = null;
- } else {
- try {
- CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
- Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
- x509Certificate = (X509Certificate) cert;
- } catch (CertificateException e) {
- x509Certificate = null;
- }
- }
- return x509Certificate;
- }
-
- private void webLogin() {
- setContentView(R.layout.activity_settings_webview);
- webView = findViewById(R.id.login_webview);
- webView.setVisibility(View.GONE);
-
- final ProgressBar progressBar = findViewById(R.id.login_webview_progress_bar);
-
- WebSettings settings = webView.getSettings();
- settings.setAllowFileAccess(false);
- settings.setJavaScriptEnabled(true);
- settings.setDomStorageEnabled(true);
- settings.setUserAgentString(getWebLoginUserAgent());
- settings.setSaveFormData(false);
- settings.setSavePassword(false);
-
- Map headers = new HashMap<>();
- headers.put("OCS-APIREQUEST", "true");
-
-
- webView.loadUrl(normalizeUrlSuffix(NotesClientUtil.formatURL(field_url.getText().toString())) + "index.php/login/flow", headers);
-
- webView.setWebViewClient(new WebViewClient() {
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- if (url.startsWith("nc://login/")) {
- parseAndLoginFromWebView(url);
- return true;
- }
- return false;
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- super.onPageFinished(view, url);
-
- progressBar.setVisibility(View.GONE);
- webView.setVisibility(View.VISIBLE);
- }
-
- @Override
- public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
- X509Certificate cert = getX509CertificateFromError(error);
-
- try {
- final boolean[] accepted = new boolean[1];
- NoteServerSyncHelper.getInstance(NoteSQLiteOpenHelper.getInstance(getApplicationContext()))
- .checkCertificate(cert.getEncoded(), true, new IOnCertificateDecision.Stub() {
- @Override
- public void accept() {
- Log.d("Note", "cert accepted");
- handler.proceed();
- accepted[0] = true;
- }
-
- @Override
- public void reject() {
- Log.d("Note", "cert rejected");
- handler.cancel();
- killProcess(myPid());
- }
- });
- } catch (Exception e) {
- Log.e("Note", "Cert could not be verified");
- handler.proceed();
- }
- }
-
- });
-
- // show snackbar after 60s to switch back to old login method
- new Handler().postDelayed(() -> {
- Snackbar.make(webView, R.string.fallback_weblogin_text, Snackbar.LENGTH_INDEFINITE)
- .setAction(R.string.fallback_weblogin_back, (View.OnClickListener) v -> initLegacyLogin(field_url.getText().toString())).show();
- }, 45 * 1000);
- }
-
- private String getWebLoginUserAgent() {
- return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
- Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL;
- }
-
- private void parseAndLoginFromWebView(String dataString) {
- String prefix = "nc://login/";
- LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString);
-
- if (loginUrlInfo != null) {
- String url = normalizeUrlSuffix(loginUrlInfo.serverAddress);
-
- new LoginValidatorAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url, loginUrlInfo.username,
- loginUrlInfo.password);
- }
- }
-
- /**
- * parses a URI string and returns a login data object with the information from the URI string.
- *
- * @param prefix URI beginning, e.g. cloud://login/
- * @param dataString the complete URI
- * @return login data
- * @throws IllegalArgumentException when
- */
- private LoginUrlInfo parseLoginDataUrl(String prefix, String dataString) throws IllegalArgumentException {
- if (dataString.length() < prefix.length()) {
- throw new IllegalArgumentException("Invalid login URL detected");
- }
- LoginUrlInfo loginUrlInfo = new LoginUrlInfo();
-
- // format is basically xxx://login/server:xxx&user:xxx&password while all variables are optional
- String data = dataString.substring(prefix.length());
-
- // parse data
- String[] values = data.split("&");
-
- if (values.length < 1 || values.length > 3) {
- // error illegal number of URL elements detected
- throw new IllegalArgumentException("Illegal number of login URL elements detected: " + values.length);
- }
-
- for (String value : values) {
- if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
- loginUrlInfo.username = URLDecoder.decode(
- value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()));
- } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
- loginUrlInfo.password = URLDecoder.decode(
- value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()));
- } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
- loginUrlInfo.serverAddress = URLDecoder.decode(
- value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()));
- } else {
- // error illegal URL element detected
- throw new IllegalArgumentException("Illegal magic login URL element detected: " + value);
- }
- }
-
- return loginUrlInfo;
- }
-
- private String normalizeUrlSuffix(String url) {
- if (url.toLowerCase(Locale.ROOT).endsWith(WEBDAV_PATH_4_0_AND_LATER)) {
- return url.substring(0, url.length() - WEBDAV_PATH_4_0_AND_LATER.length());
- }
-
- if (!url.endsWith("/")) {
- return url + "/";
- }
-
- return url;
- }
-
- private void initLegacyLogin(String oldUrl) {
- useWebLogin = false;
- new URLValidatorAsyncTask().execute(NotesClientUtil.formatURL(field_url.getText().toString()));
-
- webView.setVisibility(View.INVISIBLE);
- setContentView(R.layout.activity_settings);
-
- ButterKnife.bind(this);
- setupListener();
-
- field_url.setText(oldUrl);
- username_wrapper.setVisibility(View.VISIBLE);
- password_wrapper.setVisibility(View.VISIBLE);
- }
-
- private void handleSubmitButtonEnabled() {
- // drawable[2] is not null if url is valid, see URLValidatorAsyncTask::onPostExecute
- if (useWebLogin || field_url.getCompoundDrawables()[2] != null && (username_wrapper.getVisibility() == View.GONE ||
- (username_wrapper.getVisibility() == View.VISIBLE && field_username.getText().length() > 0))) {
- btn_submit.setEnabled(true);
- } else {
- btn_submit.setEnabled(false);
- }
- }
-
- /************************************ Async Tasks ************************************/
-
- /**
- * Checks if the given URL returns a valid status code and sets the Check next to the URL-Input Field to visible.
- * Created by stefan on 23.09.15.
- */
- private class URLValidatorAsyncTask extends AsyncTask {
-
- @Override
- protected void onPreExecute() {
- btn_submit.setEnabled(false);
- field_url.setCompoundDrawables(null, null, null, null);
- }
-
- @Override
- protected Boolean doInBackground(String... params) {
- CustomCertManager ccm = NoteServerSyncHelper.getInstance(NoteSQLiteOpenHelper.getInstance(getApplicationContext())).getCustomCertManager();
- return NotesClientUtil.isValidURL(ccm, params[0]);
- }
-
- @Override
- protected void onPostExecute(Boolean o) {
- if (o) {
- Drawable actionDoneDark = ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_check_grey600_24dp);
- actionDoneDark.setBounds(0, 0, actionDoneDark.getIntrinsicWidth(), actionDoneDark.getIntrinsicHeight());
- field_url.setCompoundDrawables(null, null, actionDoneDark, null);
- } else {
- field_url.setCompoundDrawables(null, null, null, null);
- }
- handleSubmitButtonEnabled();
- }
- }
-
- /**
- * If Log-In-Credentials are correct, save Credentials to Shared Preferences and finish First Run Wizard.
- */
- private class LoginValidatorAsyncTask extends AsyncTask {
- String url, username, password;
-
- @Override
- protected void onPreExecute() {
- setInputsEnabled(false);
- btn_submit.setText(R.string.settings_submitting);
- }
-
- /**
- * @param params url, username and password
- * @return isValidLogin Boolean
- */
- @Override
- protected LoginStatus doInBackground(String... params) {
- url = params[0];
- username = params[1];
- password = params[2];
- CustomCertManager ccm = NoteServerSyncHelper.getInstance(NoteSQLiteOpenHelper.getInstance(getApplicationContext())).getCustomCertManager();
- return NotesClientUtil.isValidLogin(ccm, url, username, password);
- }
-
- @Override
- protected void onPostExecute(LoginStatus status) {
- if (LoginStatus.OK.equals(status)) {
- SharedPreferences.Editor editor = preferences.edit();
- editor.putString(SETTINGS_URL, url);
- editor.putString(SETTINGS_USERNAME, username);
- editor.putString(SETTINGS_PASSWORD, password);
- editor.putBoolean(SETTINGS_IS_DEVICE_ACCOUNT, false);
- editor.remove(SETTINGS_KEY_ETAG);
- editor.remove(SETTINGS_KEY_LAST_MODIFIED);
- editor.apply();
-
- final Intent data = new Intent();
- data.putExtra(NotesListViewActivity.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED);
- setResult(RESULT_OK, data);
- finish();
- } else {
- Log.e("Note", "invalid login");
- btn_submit.setText(R.string.settings_submit);
- setInputsEnabled(true);
- Toast.makeText(getApplicationContext(), getString(R.string.error_invalid_login, getString(status.str)), Toast.LENGTH_LONG).show();
- }
- }
-
- /**
- * Sets all Input-Fields and Buttons to enabled or disabled depending on the given boolean.
- *
- * @param enabled - boolean
- */
- private void setInputsEnabled(boolean enabled) {
- btn_submit.setEnabled(enabled);
- field_url.setEnabled(enabled);
- field_username.setEnabled(enabled);
- field_password.setEnabled(enabled);
- }
- }
-
- /**
- * Data object holding the login url fields.
- */
- public class LoginUrlInfo {
- String serverAddress;
- String username;
- String password;
- }
-
-}
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/CreateNoteWidget.java b/app/src/main/java/foundation/e/notes/android/appwidget/CreateNoteWidget.java
deleted file mode 100644
index 237549a009bd32798515e5540e9541b3830f62d1..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/CreateNoteWidget.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.Context;
-import android.content.Intent;
-import android.widget.RemoteViews;
-
-import foundation.e.notes.R;
-import foundation.e.notes.android.activity.EditNoteActivity;
-
-/**
- * Implementation of App Widget functionality.
- */
-public class CreateNoteWidget extends AppWidgetProvider {
-
- private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
- int appWidgetId) {
-
- // Construct the RemoteViews object
- RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_create_note);
- Intent intent = new Intent(context, EditNoteActivity.class);
-
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- views.setOnClickPendingIntent(R.id.widget_create_note, pendingIntent);
-
- // Instruct the widget manager to update the widget
- appWidgetManager.updateAppWidget(appWidgetId, views);
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
-
- // There may be multiple widgets active, so update all of them
- for (int appWidgetId : appWidgetIds) {
- updateAppWidget(context, appWidgetManager, appWidgetId);
- }
- }
-
- @Override
- public void onEnabled(Context context) {
- // Enter relevant functionality for when the first widget is created
- }
-
- @Override
- public void onDisabled(Context context) {
- // Enter relevant functionality for when the last widget is disabled
- }
-}
-
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidget.java b/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidget.java
deleted file mode 100644
index ff9cb900ebb3cc469eddf97af54f399383e3013d..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidget.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.widget.RemoteViews;
-
-import foundation.e.notes.android.activity.EditNoteActivity;
-import foundation.e.notes.android.activity.NotesListViewActivity;
-import foundation.e.notes.R;
-import foundation.e.notes.util.Notes;
-
-public class NoteListWidget extends AppWidgetProvider {
- public static final String WIDGET_MODE_KEY = "NLW_mode";
- public static final String WIDGET_CATEGORY_KEY = "NLW_cat";
- public static final int NLW_DISPLAY_ALL = 0;
- public static final int NLW_DISPLAY_STARRED = 1;
- public static final int NLW_DISPLAY_CATEGORY = 2;
-
- static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
- RemoteViews views;
- boolean darkTheme;
-
- for (int appWidgetId : appWidgetIds) {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
- int displayMode = sp.getInt(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, -1);
-
- // onUpdate has been triggered before the user finished configuring the widget
- if (displayMode == -1) {
- return;
- }
-
- String category = sp.getString(NoteListWidget.WIDGET_CATEGORY_KEY + appWidgetId, null);
- darkTheme = Notes.getAppTheme(context);
-
- Intent serviceIntent = new Intent(context, NoteListWidgetService.class);
- serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- serviceIntent.putExtra(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, displayMode);
- serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
-
- if (displayMode == NLW_DISPLAY_CATEGORY) {
- serviceIntent.putExtra(NoteListWidget.WIDGET_CATEGORY_KEY + appWidgetId, category);
- }
-
- // Launch application when user taps the header icon or app title
- Intent intent = new Intent("android.intent.action.MAIN");
- intent.setComponent(new ComponentName(context.getPackageName(),
- NotesListViewActivity.class.getName()));
-
- // Open the main app if the user taps the widget header
- PendingIntent openAppI = PendingIntent.getActivity(context, 0, intent,
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Launch create note activity if user taps "+" icon on header
- PendingIntent newNoteI = PendingIntent.getActivity(context, 0,
- (new Intent(context, EditNoteActivity.class)),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- PendingIntent templatePI = PendingIntent.getActivity(context, 0,
- (new Intent(context, EditNoteActivity.class)),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- if (darkTheme) {
- views = new RemoteViews(context.getPackageName(), foundation.e.notes.R.layout.widget_note_list_dark);
- views.setTextViewText(foundation.e.notes.R.id.widget_note_list_title_tv_dark, getWidgetTitle(context, displayMode, category));
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_header_icon_dark, openAppI);
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_list_title_tv_dark, openAppI);
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_list_create_icon_dark, newNoteI);
- views.setPendingIntentTemplate(foundation.e.notes.R.id.note_list_widget_lv_dark, templatePI);
- views.setRemoteAdapter(appWidgetId, foundation.e.notes.R.id.note_list_widget_lv_dark, serviceIntent);
- views.setEmptyView(foundation.e.notes.R.id.note_list_widget_lv_dark, foundation.e.notes.R.id.widget_note_list_placeholder_tv_dark);
- awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv_dark);
- } else {
- views = new RemoteViews(context.getPackageName(), foundation.e.notes.R.layout.widget_note_list);
- views.setTextViewText(foundation.e.notes.R.id.widget_note_list_title_tv, getWidgetTitle(context, displayMode, category));
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_header_icon, openAppI);
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_list_title_tv, openAppI);
- views.setOnClickPendingIntent(foundation.e.notes.R.id.widget_note_list_create_icon, newNoteI);
- views.setPendingIntentTemplate(foundation.e.notes.R.id.note_list_widget_lv, templatePI);
- views.setRemoteAdapter(appWidgetId, foundation.e.notes.R.id.note_list_widget_lv, serviceIntent);
- views.setEmptyView(foundation.e.notes.R.id.note_list_widget_lv, foundation.e.notes.R.id.widget_note_list_placeholder_tv);
- awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv);
- }
-
- awm.updateAppWidget(appWidgetId, views);
- }
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- super.onUpdate(context, appWidgetManager, appWidgetIds);
- updateAppWidget(context, appWidgetManager, appWidgetIds);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- super.onReceive(context, intent);
- AppWidgetManager awm = AppWidgetManager.getInstance(context);
-
- if (intent.getAction() != null) {
- if (intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
- if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
- if (intent.getExtras() != null) {
- updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)});
- } else {
- Log.w(NoteListWidget.class.getSimpleName(), "intent.getExtras() is null");
- }
- } else {
- updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class)));
- }
- }
- } else {
- Log.w(NoteListWidget.class.getSimpleName(), "intent.getAction() is null");
- }
- }
-
- @Override
- public void onDeleted(Context context, int[] appWidgetIds) {
- super.onDeleted(context, appWidgetIds);
-
- SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
-
- for (int appWidgetId : appWidgetIds) {
- editor.remove(WIDGET_MODE_KEY + appWidgetId);
- editor.remove(WIDGET_CATEGORY_KEY + appWidgetId);
- }
-
- editor.apply();
- }
-
- private static String getWidgetTitle(Context context, int displayMode, String category) {
- switch (displayMode) {
- case NoteListWidget.NLW_DISPLAY_ALL:
- return context.getString(R.string.app_name);
- case NoteListWidget.NLW_DISPLAY_STARRED:
- return context.getString(R.string.label_favorites);
- case NoteListWidget.NLW_DISPLAY_CATEGORY:
- if (category.equals("")) {
- return context.getString(foundation.e.notes.R.string.action_uncategorized);
- } else {
- return category;
- }
- }
-
- return null;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetConfiguration.java b/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetConfiguration.java
deleted file mode 100644
index d72830af4e648a4971c5c206aac8de442b4e0b5a..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetConfiguration.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.app.Activity;
-import android.appwidget.AppWidgetManager;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import foundation.e.notes.android.activity.NotesListViewActivity;
-import foundation.e.notes.model.NavigationAdapter;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-import foundation.e.notes.persistence.NoteServerSyncHelper;
-
-public class NoteListWidgetConfiguration extends AppCompatActivity {
- private static final String TAG = Activity.class.getSimpleName();
-
- private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
-
- private NavigationAdapter adapterCategories;
- private NavigationAdapter.NavigationItem itemRecent, itemFavorites;
- private NoteSQLiteOpenHelper db = null;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setResult(RESULT_CANCELED);
- setContentView(foundation.e.notes.R.layout.activity_note_list_configuration);
-
- if (!(NoteServerSyncHelper.isConfigured(this))) {
- Toast.makeText(this, foundation.e.notes.R.string.widget_not_logged_in, Toast.LENGTH_LONG).show();
-
- // TODO Present user with app login screen
- Log.w(TAG, "onCreate: user not logged in");
- finish();
- }
-
- db = NoteSQLiteOpenHelper.getInstance(this);
- final Bundle extras = getIntent().getExtras();
-
- if (extras != null) {
- appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID);
- }
-
- if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
- Log.d(TAG, "INVALID_APPWIDGET_ID");
- finish();
- }
-
- itemRecent = new NavigationAdapter.NavigationItem(NotesListViewActivity.ADAPTER_KEY_RECENT,
- getString(foundation.e.notes.R.string.label_all_notes),
- null,
- foundation.e.notes.R.drawable.ic_access_time_grey600_24dp);
- itemFavorites = new NavigationAdapter.NavigationItem(NotesListViewActivity.ADAPTER_KEY_STARRED,
- getString(foundation.e.notes.R.string.label_favorites),
- null,
- foundation.e.notes.R.drawable.ic_star_yellow_24dp);
- RecyclerView recyclerView;
- RecyclerView.LayoutManager layoutManager;
-
- adapterCategories = new NavigationAdapter(new NavigationAdapter.ClickListener() {
- @Override
- public void onItemClick(NavigationAdapter.NavigationItem item) {
- SharedPreferences.Editor sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit();
-
- if (item == itemRecent) {
- sp.putInt(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, NoteListWidget.NLW_DISPLAY_ALL);
- } else if (item == itemFavorites) {
- sp.putInt(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, NoteListWidget.NLW_DISPLAY_STARRED);
- } else {
- String category = "";
- if (!item.label.equals(getString(foundation.e.notes.R.string.action_uncategorized))) {
- category = item.label;
- }
- sp.putInt(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, NoteListWidget.NLW_DISPLAY_CATEGORY);
- sp.putString(NoteListWidget.WIDGET_CATEGORY_KEY + appWidgetId, category);
- }
- sp.apply();
-
- Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
- getApplicationContext(), NoteListWidget.class);
- updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- setResult(RESULT_OK, updateIntent);
- getApplicationContext().sendBroadcast(updateIntent);
- finish();
- }
-
- public void onIconClick(NavigationAdapter.NavigationItem item) {
- onItemClick(item);
- }
- });
-
- recyclerView = findViewById(foundation.e.notes.R.id.nlw_config_recyclerv);
- recyclerView.setHasFixedSize(true);
- layoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(adapterCategories);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- new LoadCategoryListTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private class LoadCategoryListTask extends AsyncTask> {
- @Override
- protected List doInBackground(Void... voids) {
- NavigationAdapter.NavigationItem itemUncategorized;
- List categories = db.getCategories();
-
- if (!categories.isEmpty() && categories.get(0).label.isEmpty()) {
- itemUncategorized = categories.get(0);
- itemUncategorized.label = getString(foundation.e.notes.R.string.action_uncategorized);
- itemUncategorized.icon = NavigationAdapter.ICON_NOFOLDER;
- }
-
- Map favorites = db.getFavoritesCount();
- int numFavorites = favorites.containsKey("1") ? favorites.get("1") : 0;
- int numNonFavorites = favorites.containsKey("0") ? favorites.get("0") : 0;
- itemFavorites.count = numFavorites;
- itemRecent.count = numFavorites + numNonFavorites;
-
- ArrayList items = new ArrayList<>();
- items.add(itemRecent);
- items.add(itemFavorites);
-
- for (NavigationAdapter.NavigationItem item : categories) {
- int slashIndex = item.label.indexOf('/');
-
- item.label = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
- item.id = "category:" + item.label;
- items.add(item);
- }
- return items;
- }
-
- @Override
- protected void onPostExecute(List items) {
- adapterCategories.setItems(items);
- }
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetFactory.java b/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetFactory.java
deleted file mode 100644
index d2bab203ef039c032af443a74f62908a8c08af37..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/NoteListWidgetFactory.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.widget.RemoteViews;
-import android.widget.RemoteViewsService;
-
-import java.util.List;
-
-import foundation.e.notes.R;
-import foundation.e.notes.android.activity.EditNoteActivity;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-import foundation.e.notes.util.Notes;
-
-public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
- private final Context context;
- private final int displayMode;
- private final int appWidgetId;
- private final boolean darkTheme;
- private String category;
- private final SharedPreferences sp;
- private NoteSQLiteOpenHelper db;
- private List dbNotes;
-
- NoteListWidgetFactory(Context context, Intent intent) {
- this.context = context;
- appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID);
- sp = PreferenceManager.getDefaultSharedPreferences(this.context);
- displayMode = sp.getInt(NoteListWidget.WIDGET_MODE_KEY + appWidgetId, -1);
- darkTheme = Notes.getAppTheme(this.context);
- category = sp.getString(NoteListWidget.WIDGET_CATEGORY_KEY + appWidgetId, "");
- }
-
- @Override
- public void onCreate() {
- db = NoteSQLiteOpenHelper.getInstance(context);
- }
-
- @Override
- public void onDataSetChanged() {
- if (displayMode == NoteListWidget.NLW_DISPLAY_ALL) {
- dbNotes = db.getNotes();
- } else if (displayMode == NoteListWidget.NLW_DISPLAY_STARRED) {
- dbNotes = db.searchNotes(null,null, true);
- } else if (displayMode == NoteListWidget.NLW_DISPLAY_CATEGORY) {
- dbNotes = db.searchNotes(null, category, null);
- }
- }
-
- @Override
- public void onDestroy() {
-
- }
-
- /**
- * getCount()
- *
- * @return Total number of entries
- */
- @Override
- public int getCount() {
- if (dbNotes == null) {
- return 0;
- }
-
- return dbNotes.size();
- }
-
- @Override
- public RemoteViews getViewAt(int i) {
- RemoteViews note_content;
-
- if (dbNotes == null || dbNotes.get(i) == null) {
- return null;
- }
-
- DBNote note = dbNotes.get(i);
- final Intent fillInIntent = new Intent();
- final Bundle extras = new Bundle();
-
- extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId());
- fillInIntent.putExtras(extras);
- fillInIntent.setData(Uri.parse(fillInIntent.toUri(Intent.URI_INTENT_SCHEME)));
-
- if (darkTheme) {
- note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry_dark);
- note_content.setOnClickFillInIntent(R.id.widget_note_list_entry_dark, fillInIntent);
- note_content.setTextViewText(R.id.widget_entry_content_tv_dark, note.getTitle());
-
- if (note.isFavorite()) {
- note_content.setImageViewResource(R.id.widget_entry_fav_icon_dark, R.drawable.ic_star_yellow_24dp);
- } else {
- note_content.setImageViewResource(R.id.widget_entry_fav_icon_dark, R.drawable.ic_star_border_white_24dp);
- }
- } else {
- note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry);
- note_content.setOnClickFillInIntent(R.id.widget_note_list_entry, fillInIntent);
- note_content.setTextViewText(R.id.widget_entry_content_tv, note.getTitle());
-
- if (note.isFavorite()) {
- note_content.setImageViewResource(R.id.widget_entry_fav_icon, R.drawable.ic_star_yellow_24dp);
- } else {
- note_content.setImageViewResource(R.id.widget_entry_fav_icon, R.drawable.ic_star_border_white_24dp);
- }
- }
-
- return note_content;
-
- }
-
- @Override
- public RemoteViews getLoadingView() {
- return null;
- }
-
- @Override
- public int getViewTypeCount() {
- return 1;
- }
-
- @Override
- public long getItemId(int i) {
- return i;
- }
-
- @Override
- public boolean hasStableIds() {
- return true;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidget.java b/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidget.java
deleted file mode 100644
index fa7eaeede7d27299e8572cd76417febee902eba7..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidget.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.preference.PreferenceManager;
-import android.widget.RemoteViews;
-
-import foundation.e.notes.R;
-import foundation.e.notes.android.activity.EditNoteActivity;
-import foundation.e.notes.util.Notes;
-
-public class SingleNoteWidget extends AppWidgetProvider {
- private static boolean darkTheme;
-
- public static final String WIDGET_KEY = "single_note_widget";
-
- static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
- Intent templateIntent = new Intent(context, EditNoteActivity.class);
- templateIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
-
- for (int appWidgetId : appWidgetIds) {
- // onUpdate has been triggered before the user finished configuring the widget
- if ((sp.getLong(WIDGET_KEY + appWidgetId, -1)) == -1) {
- return;
- }
-
- darkTheme = Notes.getAppTheme(context);
-
- PendingIntent templatePendingIntent = PendingIntent.getActivity(context, appWidgetId, templateIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- Intent serviceIntent = new Intent(context, SingleNoteWidgetService.class);
- serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
- serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
-
- RemoteViews views;
-
- if (darkTheme) {
- views = new RemoteViews(context.getPackageName(), foundation.e.notes.R.layout.widget_single_note_dark);
- views.setPendingIntentTemplate(foundation.e.notes.R.id.single_note_widget_lv_dark, templatePendingIntent);
- views.setRemoteAdapter(foundation.e.notes.R.id.single_note_widget_lv_dark, serviceIntent);
- views.setEmptyView(foundation.e.notes.R.id.single_note_widget_lv_dark, foundation.e.notes.R.id.widget_single_note_placeholder_tv_dark);
- awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv_dark);
- } else {
- views = new RemoteViews(context.getPackageName(), foundation.e.notes.R.layout.widget_single_note);
- views.setPendingIntentTemplate(foundation.e.notes.R.id.single_note_widget_lv, templatePendingIntent);
- views.setRemoteAdapter(foundation.e.notes.R.id.single_note_widget_lv, serviceIntent);
- views.setEmptyView(foundation.e.notes.R.id.single_note_widget_lv, foundation.e.notes.R.id.widget_single_note_placeholder_tv);
- awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.single_note_widget_lv);
- }
-
- awm.updateAppWidget(appWidgetId, views);
- }
- }
-
- @Override
- public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- super.onUpdate(context, appWidgetManager, appWidgetIds);
- updateAppWidget(context, appWidgetManager, appWidgetIds);
- }
-
- @Override
- public void onReceive(Context context, Intent intent) {
- super.onReceive(context, intent);
- AppWidgetManager awm = AppWidgetManager.getInstance(context);
-
- updateAppWidget(context, AppWidgetManager.getInstance(context),
- (awm.getAppWidgetIds(new ComponentName(context, SingleNoteWidget.class))));
- }
-
- @Override
- public void onDeleted(Context context, int[] appWidgetIds) {
- SharedPreferences.Editor editor = PreferenceManager
- .getDefaultSharedPreferences(context).edit();
-
- for (int appWidgetId : appWidgetIds) {
- editor.remove(WIDGET_KEY + appWidgetId);
- }
-
- editor.apply();
- super.onDeleted(context, appWidgetIds);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidgetFactory.java b/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidgetFactory.java
deleted file mode 100644
index 0ed9f3b8cdaa8843e4b1b696406af78eeac5427e..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/appwidget/SingleNoteWidgetFactory.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package foundation.e.notes.android.appwidget;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.widget.RemoteViews;
-import android.widget.RemoteViewsService;
-
-import com.yydcdut.markdown.MarkdownProcessor;
-import com.yydcdut.markdown.syntax.text.TextFactory;
-
-import foundation.e.notes.R;
-import foundation.e.notes.android.activity.EditNoteActivity;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-import foundation.e.notes.util.MarkDownUtil;
-import foundation.e.notes.util.Notes;
-
-public class SingleNoteWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
-
- private MarkdownProcessor markdownProcessor;
- private final Context context;
- private final int appWidgetId;
-
- private NoteSQLiteOpenHelper db;
- private DBNote note;
- private SharedPreferences sp;
- private static Boolean darkTheme;
-
- private static final String TAG = SingleNoteWidget.class.getSimpleName();
-
- SingleNoteWidgetFactory(Context context, Intent intent) {
- this.context = context;
- appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID);
- sp = PreferenceManager.getDefaultSharedPreferences(this.context);
- darkTheme = Notes.getAppTheme(this.context);
- markdownProcessor = new MarkdownProcessor(this.context);
- markdownProcessor.factory(TextFactory.create());
- markdownProcessor.config(MarkDownUtil.getMarkDownConfiguration(this.context, darkTheme).build());
- }
-
- @Override
- public void onCreate() {
- db = NoteSQLiteOpenHelper.getInstance(context);
- }
-
-
- @Override
- public void onDataSetChanged() {
- long noteID = sp.getLong(SingleNoteWidget.WIDGET_KEY + appWidgetId, -1);
-
- if (noteID >= 0) {
- note = db.getNote(noteID);
-
- if (note == null) {
- Log.e(TAG, "Error: note not found");
- }
- }
- }
-
- @Override
- public void onDestroy() {
-
- }
-
- /**
- * Returns the number of items in the data set. In this case, always 1 as a single note is
- * being displayed. Will return 0 when the note can't be displayed.
- */
- @Override
- public int getCount() {
- return (note != null) ? 1 : 0;
- }
-
- /**
- * Returns a RemoteView containing the note content in a TextView and
- * a fillInIntent to handle the user tapping on the item in the list view.
- *
- * @param position The position of the item in the list
- * @return The RemoteView at the specified position in the list
- */
- @Override
- public RemoteViews getViewAt(int position) {
- if (note == null) {
- return null;
- }
-
- RemoteViews note_content;
-
- final Intent fillInIntent = new Intent();
- final Bundle extras = new Bundle();
-
- extras.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId());
- fillInIntent.putExtras(extras);
- fillInIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- if (darkTheme) {
- note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content_dark);
- note_content.setOnClickFillInIntent(R.id.single_note_content_tv_dark, fillInIntent);
- note_content.setTextViewText(R.id.single_note_content_tv_dark, markdownProcessor.parse(note.getContent()));
-
- } else {
- note_content = new RemoteViews(context.getPackageName(), R.layout.widget_single_note_content);
- note_content.setOnClickFillInIntent(R.id.single_note_content_tv, fillInIntent);
- note_content.setTextViewText(R.id.single_note_content_tv, markdownProcessor.parse(note.getContent()));
- }
-
- return note_content;
- }
-
-
- // TODO Set loading view
- @Override
- public RemoteViews getLoadingView() {
- return null;
- }
-
- @Override
- public int getViewTypeCount() {
- return 1;
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public boolean hasStableIds() {
- return true;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/BaseNoteFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/BaseNoteFragment.java
deleted file mode 100644
index ade35d1a21e951bc5150eb654abdba04637774ee..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/BaseNoteFragment.java
+++ /dev/null
@@ -1,363 +0,0 @@
-package foundation.e.notes.android.fragment;
-
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.Bundle;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.ShareActionProvider;
-import androidx.core.view.MenuItemCompat;
-import androidx.core.view.ViewCompat;
-
-import foundation.e.notes.R;
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-import foundation.e.notes.util.ICallback;
-import foundation.e.notes.android.activity.EditNoteActivity;
-import foundation.e.notes.util.DisplayUtils;
-
-import static androidx.core.content.pm.ShortcutManagerCompat.isRequestPinShortcutSupported;
-import static foundation.e.notes.android.activity.EditNoteActivity.ACTION_SHORTCUT;
-
-public abstract class BaseNoteFragment extends Fragment implements CategoryDialogFragment.CategoryDialogListener {
-
- public interface NoteFragmentListener {
- void close();
-
- void onNoteUpdated(DBNote note);
- }
-
- private static final int MENU_ID_PIN = -1;
- public static final String PARAM_NOTE_ID = "noteId";
- public static final String PARAM_NEWNOTE = "newNote";
- private static final String SAVEDKEY_NOTE = "note";
- private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note";
-
- protected SearchView searchView;
- protected MenuItem searchMenuItem;
-
- protected String searchQuery = null;
-
- protected DBNote note;
- @Nullable
- private DBNote originalNote;
- private NoteSQLiteOpenHelper db;
- private NoteFragmentListener listener;
-
- private TextView activeTextView;
- private boolean isNew;
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (savedInstanceState != null) {
- searchQuery = savedInstanceState.getString("searchQuery", "");
- }
-
- }
-
- protected void setActiveTextView(TextView textView) {
- activeTextView = textView;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (savedInstanceState == null) {
- isNew = true;
- long id = getArguments().getLong(PARAM_NOTE_ID);
- if (id > 0) {
- note = originalNote = db.getNote(id);
- } else {
- CloudNote cloudNote = (CloudNote) getArguments().getSerializable(PARAM_NEWNOTE);
- if (cloudNote == null) {
- throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given and argument " + PARAM_NEWNOTE + " is missing.");
- }
- note = db.getNote(db.addNoteAndSync(cloudNote));
- originalNote = null;
- }
- } else {
- isNew = false;
- note = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
- originalNote = (DBNote) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
- }
- setHasOptionsMenu(true);
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- try {
- listener = (NoteFragmentListener) activity;
- } catch (ClassCastException e) {
- throw new ClassCastException(activity.getClass() + " must implement " + NoteFragmentListener.class);
- }
- db = NoteSQLiteOpenHelper.getInstance(activity);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- listener.onNoteUpdated(note);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- saveNote(null);
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- listener = null;
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- saveNote(null);
- outState.putSerializable(SAVEDKEY_NOTE, note);
- outState.putSerializable(SAVEDKEY_ORIGINAL_NOTE, originalNote);
-
- if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) {
- outState.putString("searchQuery", searchView.getQuery().toString());
- }
- }
-
- private void colorWithText(String newText) {
- if (activeTextView != null && ViewCompat.isAttachedToWindow(activeTextView)) {
- activeTextView.setText(DisplayUtils.searchAndColor(activeTextView.getText().toString(), new SpannableString
- (activeTextView.getText()), newText, getResources().getColor(R.color.color_default_primary_text)),
- TextView.BufferType.SPANNABLE);
- }
- }
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- inflater.inflate(R.menu.menu_note_fragment, menu);
-
- if (isRequestPinShortcutSupported(getActivity()) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- menu.add(Menu.NONE, MENU_ID_PIN, 110, R.string.pin_to_homescreen);
- }
- }
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
- MenuItem itemFavorite = menu.findItem(R.id.menu_favorite);
- prepareFavoriteOption(itemFavorite);
-
- searchMenuItem = menu.findItem(R.id.search);
- searchView = (SearchView) searchMenuItem.getActionView();
-
- if (!TextUtils.isEmpty(searchQuery) && isNew) {
- searchMenuItem.expandActionView();
- searchView.setQuery(searchQuery, true);
- searchView.clearFocus();
- } else {
- searchMenuItem.collapseActionView();
- }
-
-
- final LinearLayout searchEditFrame = 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 != View.VISIBLE) {
- colorWithText("");
- searchQuery = "";
- }
-
- oldVisibility = currentVisibility;
- }
- }
-
- });
-
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String newText) {
- searchQuery = newText;
- colorWithText(newText);
- return true;
- }
- });
-
- }
-
- private void prepareFavoriteOption(MenuItem item) {
- item.setIcon(note.isFavorite() ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp);
- item.setChecked(note.isFavorite());
- }
-
- /**
- * Main-Menu-Handler
- */
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_cancel:
- if (originalNote == null) {
- db.deleteNoteAndSync(note.getId());
- } else {
- db.updateNoteAndSync(originalNote, null, null);
- }
- listener.close();
- return true;
- case R.id.menu_delete:
- db.deleteNoteAndSync(note.getId());
- listener.close();
- return true;
- case R.id.menu_favorite:
- db.toggleFavorite(note, null);
- listener.onNoteUpdated(note);
- prepareFavoriteOption(item);
- return true;
- case R.id.menu_category:
- showCategorySelector();
- return true;
- case R.id.menu_share:
- Intent shareIntent = new Intent();
- shareIntent.setAction(Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, note.getTitle());
- shareIntent.putExtra(android.content.Intent.EXTRA_TEXT, note.getContent());
-
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- startActivity(Intent.createChooser(shareIntent, note.getTitle()));
- } else {
- ShareActionProvider actionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(item);
- actionProvider.setShareIntent(shareIntent);
- }
-
- return false;
- case MENU_ID_PIN:
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- ShortcutManager shortcutManager = getActivity().getSystemService(ShortcutManager.class);
-
- if (shortcutManager.isRequestPinShortcutSupported()) {
- Intent intent = new Intent(getActivity(), EditNoteActivity.class);
- intent.putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId());
- intent.setAction(ACTION_SHORTCUT);
-
- ShortcutInfo pinShortcutInfo = new ShortcutInfo.Builder(getActivity(), note.getId() + "")
- .setShortLabel(note.getTitle())
- .setIcon(Icon.createWithResource(getActivity().getApplicationContext(), note.isFavorite() ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_border_white_24dp))
- .setIntent(intent)
- .build();
-
- Intent pinnedShortcutCallbackIntent =
- shortcutManager.createShortcutResultIntent(pinShortcutInfo);
-
- PendingIntent successCallback = PendingIntent.getBroadcast(getActivity(), /* request code */ 0,
- pinnedShortcutCallbackIntent, /* flags */ 0);
-
- shortcutManager.requestPinShortcut(pinShortcutInfo,
- successCallback.getIntentSender());
- }
- }
-
- return true;
- default:
- return super.onOptionsItemSelected(item);
- }
- }
-
- public void onCloseNote() {
- if (originalNote == null && getContent().isEmpty()) {
- db.deleteNoteAndSync(note.getId());
- }
- }
-
- /**
- * Save the current state in the database and schedule synchronization if needed.
- *
- * @param callback Observer which is called after save/synchronization
- */
- protected void saveNote(@Nullable ICallback callback) {
- Log.d(getClass().getSimpleName(), "saveData()");
- String newContent = getContent();
- if (note.getContent().equals(newContent)) {
- Log.v(getClass().getSimpleName(), "... not saving, since nothing has changed");
- } else {
- note = db.updateNoteAndSync(note, newContent, callback);
- listener.onNoteUpdated(note);
- }
- }
-
- protected float getFontSizeFromPreferences(SharedPreferences sp) {
- final String prefValueSmall = getString(R.string.pref_value_font_size_small);
- final String prefValueMedium = getString(R.string.pref_value_font_size_medium);
- final String prefValueLarge = getString(R.string.pref_value_font_size_large);
- String fontSize = sp.getString(getString(R.string.pref_key_font_size), prefValueMedium);
-
- if (fontSize.equals(prefValueSmall)) {
- return getResources().getDimension(R.dimen.note_font_size_small);
- } else if (fontSize.equals(prefValueMedium)) {
- return getResources().getDimension(R.dimen.note_font_size_medium);
- } else {
- return getResources().getDimension(R.dimen.note_font_size_large);
- }
- }
-
- protected abstract String getContent();
-
- /**
- * Opens a dialog in order to chose a category
- */
- private void showCategorySelector() {
- final String fragmentId = "fragment_category";
- FragmentManager manager = getFragmentManager();
- Fragment frag = manager.findFragmentByTag(fragmentId);
- if (frag != null) {
- manager.beginTransaction().remove(frag).commit();
- }
- Bundle arguments = new Bundle();
- arguments.putString(CategoryDialogFragment.PARAM_CATEGORY, note.getCategory());
- CategoryDialogFragment categoryFragment = new CategoryDialogFragment();
- categoryFragment.setArguments(arguments);
- categoryFragment.setTargetFragment(this, 0);
- categoryFragment.show(manager, fragmentId);
- }
-
- @Override
- public void onCategoryChosen(String category) {
- db.setCategory(note, category, null);
- listener.onNoteUpdated(note);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/CategoryDialogFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/CategoryDialogFragment.java
deleted file mode 100644
index 6448e31dea9e9115a83a547d158f66752bd54022..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/CategoryDialogFragment.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package foundation.e.notes.android.fragment;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.WindowManager;
-import android.widget.ArrayAdapter;
-import android.widget.Filter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import androidx.annotation.NonNull;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.android.AlwaysAutoCompleteTextView;
-import foundation.e.notes.model.NavigationAdapter;
-import foundation.e.notes.persistence.NoteSQLiteOpenHelper;
-
-/**
- * This {@link DialogFragment} allows for the selection of a category.
- * It targetFragment is set it must implement the interface {@link CategoryDialogListener}.
- * The calling Activity must implement the interface {@link CategoryDialogListener}.
- */
-public class CategoryDialogFragment extends DialogFragment {
-
- /**
- * Interface that must be implemented by the calling Activity.
- */
- public interface CategoryDialogListener {
- /**
- * This method is called after the user has chosen a category.
- *
- * @param category Name of the category which was chosen by the user.
- */
- void onCategoryChosen(String category);
- }
-
- public static final String PARAM_CATEGORY = "category";
-
- @BindView(R.id.editCategory)
- AlwaysAutoCompleteTextView textCategory;
- private FolderArrayAdapter adapter;
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_change_category, null);
- ButterKnife.bind(this, dialogView);
- if (savedInstanceState == null) {
- textCategory.setText(getArguments().getString(PARAM_CATEGORY));
- }
- adapter = new FolderArrayAdapter(getActivity(), android.R.layout.simple_spinner_dropdown_item);
- textCategory.setAdapter(adapter);
- new LoadCategoriesTask().execute();
- return new AlertDialog.Builder(getActivity(), R.style.ocAlertDialog)
- .setTitle(R.string.change_category_title)
- .setView(dialogView)
- .setCancelable(true)
- .setPositiveButton(R.string.action_edit_save, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- CategoryDialogListener listener;
- Fragment target = getTargetFragment();
- if (target instanceof CategoryDialogListener) {
- listener = (CategoryDialogListener) target;
- } else {
- listener = (CategoryDialogListener) getActivity();
- }
- listener.onCategoryChosen(textCategory.getText().toString());
- }
- })
- .setNegativeButton(R.string.simple_cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // do nothing
- }
- })
- .create();
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- if (getDialog().getWindow() != null) {
- getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- } else {
- Log.w(CategoryDialogFragment.class.getSimpleName(), "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null");
- }
- }
-
-
- private class LoadCategoriesTask extends AsyncTask> {
- @Override
- protected List doInBackground(Void... voids) {
- NoteSQLiteOpenHelper db = NoteSQLiteOpenHelper.getInstance(getActivity());
- List items = db.getCategories();
- List categories = new ArrayList<>();
- for (NavigationAdapter.NavigationItem item : items) {
- if (!item.label.isEmpty()) {
- categories.add(item.label);
- }
- }
- return categories;
- }
-
- @Override
- protected void onPostExecute(List categories) {
- adapter.setData(categories);
- if (textCategory.getText().length() == 0) {
- textCategory.showFullDropDown();
- } else {
- textCategory.dismissDropDown();
- }
- }
- }
-
-
- private static class FolderArrayAdapter extends ArrayAdapter {
-
- private List originalData = new ArrayList<>();
- private Filter filter;
-
- private FolderArrayAdapter(@NonNull Context context, int resource) {
- super(context, resource);
- }
-
- public void setData(List data) {
- originalData = data;
- clear();
- addAll(data);
- }
-
- @NonNull
- @Override
- public Filter getFilter() {
- if (filter == null) {
- filter = new FolderFilter();
- }
- return filter;
- }
-
- /* This implementation is based on ArrayAdapter.ArrayFilter */
- private class FolderFilter extends Filter {
- @Override
- protected FilterResults performFiltering(CharSequence prefix) {
- final FilterResults results = new FilterResults();
-
- if (prefix == null || prefix.length() == 0) {
- final ArrayList list = new ArrayList<>(originalData);
- results.values = list;
- results.count = list.size();
- } else {
- final String prefixString = prefix.toString().toLowerCase();
- final int count = originalData.size();
- final ArrayList newValues = new ArrayList<>();
-
- for (int i = 0; i < count; i++) {
- final String value = originalData.get(i);
- final String valueText = value.toLowerCase();
-
- // First match against the whole, non-splitted value
- if (valueText.startsWith(prefixString)) {
- newValues.add(value);
- } else {
- final String[] words = valueText.split("/");
- for (String word : words) {
- if (word.startsWith(prefixString)) {
- newValues.add(value);
- break;
- }
- }
- }
- }
-
- results.values = newValues;
- results.count = newValues.size();
- }
-
- return results;
- }
-
- @Override
- protected void publishResults(CharSequence constraint, FilterResults results) {
- clear();
- addAll((List) results.values);
- if (results.count > 0) {
- notifyDataSetChanged();
- } else {
- notifyDataSetInvalidated();
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/NoteEditFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/NoteEditFragment.java
deleted file mode 100644
index 3d3f9785baab3864021b5e3d683d692552d40a03..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/NoteEditFragment.java
+++ /dev/null
@@ -1,229 +0,0 @@
-package foundation.e.notes.android.fragment;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.preference.PreferenceManager;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.yydcdut.markdown.syntax.edit.EditFactory;
-import com.yydcdut.rxmarkdown.RxMDEditText;
-import com.yydcdut.rxmarkdown.RxMarkdown;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.util.ICallback;
-import foundation.e.notes.util.MarkDownUtil;
-import foundation.e.notes.util.StyleCallback;
-import rx.Subscriber;
-
-public class NoteEditFragment extends BaseNoteFragment {
-
- private static final String LOG_TAG_AUTOSAVE = "AutoSave";
-
- private static final long DELAY = 2000; // Wait for this time after typing before saving
- private static final long DELAY_AFTER_SYNC = 5000; // Wait for this time after saving before checking for next save
- @BindView(R.id.editContent)
- RxMDEditText editContent;
- private Handler handler;
- private boolean saveActive, unsavedEdit;
- private final Runnable runAutoSave = new Runnable() {
- @Override
- public void run() {
- if (unsavedEdit) {
- Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: start AutoSave");
- autoSave();
- } else {
- Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: nothing changed");
- }
- }
- };
- private final TextWatcher textWatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(final CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(final Editable s) {
- unsavedEdit = true;
- if (!saveActive) {
- handler.removeCallbacks(runAutoSave);
- handler.postDelayed(runAutoSave, DELAY);
- }
- }
- };
-
- public static NoteEditFragment newInstance(long noteId) {
- NoteEditFragment f = new NoteEditFragment();
- Bundle b = new Bundle();
- b.putLong(PARAM_NOTE_ID, noteId);
- f.setArguments(b);
- return f;
- }
-
- public static NoteEditFragment newInstanceWithNewNote(CloudNote newNote) {
- NoteEditFragment f = new NoteEditFragment();
- Bundle b = new Bundle();
- b.putSerializable(PARAM_NEWNOTE, newNote);
- f.setArguments(b);
- return f;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- handler = new Handler(Looper.getMainLooper());
- }
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
- menu.findItem(R.id.menu_edit).setVisible(false);
- menu.findItem(R.id.menu_preview).setVisible(true);
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
- return inflater.inflate(R.layout.activity_edit, container, false);
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if(getView() != null) {
- ButterKnife.bind(this, getView());
-
- setActiveTextView(editContent);
-
- if (note.getContent().isEmpty()) {
- editContent.requestFocus();
-
- getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
-
- InputMethodManager imm = (InputMethodManager)
- getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(getView(), InputMethodManager.SHOW_IMPLICIT);
-
- }
-
- // workaround for issue yydcdut/RxMarkdown#41
- note.setContent(note.getContent().replace("\r\n", "\n"));
-
- editContent.setText(note.getContent());
- editContent.setEnabled(true);
-
- RxMarkdown.live(editContent)
- .config(MarkDownUtil.getMarkDownConfiguration(editContent.getContext()).build())
- .factory(EditFactory.create())
- .intoObservable()
- .subscribe(new Subscriber() {
- @Override
- public void onCompleted() {
- }
-
- @Override
- public void onError(Throwable e) {
- }
-
- @Override
- public void onNext(CharSequence charSequence) {
- editContent.setText(charSequence, TextView.BufferType.SPANNABLE);
- }
- });
-
- editContent.setCustomSelectionActionModeCallback(new StyleCallback(this.editContent));
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
- editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(sp));
- if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
- editContent.setTypeface(Typeface.MONOSPACE);
- }
- } else {
- Log.e(NoteEditFragment.class.getSimpleName(), "getView() is null");
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- editContent.addTextChangedListener(textWatcher);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- editContent.removeTextChangedListener(textWatcher);
- cancelTimers();
- }
-
- private void cancelTimers() {
- handler.removeCallbacks(runAutoSave);
- }
-
- /**
- * Gets the current content of the EditText field in the UI.
- *
- * @return String of the current content.
- */
- @Override
- protected String getContent() {
- return editContent.getText().toString();
- }
-
- @Override
- protected void saveNote(@Nullable ICallback callback) {
- super.saveNote(callback);
- unsavedEdit = false;
- }
-
- /**
- * Saves the current changes and show the status in the ActionBar
- */
- private void autoSave() {
- Log.d(LOG_TAG_AUTOSAVE, "STARTAUTOSAVE");
- saveActive = true;
- saveNote(new ICallback() {
- @Override
- public void onFinish() {
- onSaved();
- }
-
- @Override
- public void onScheduled() {
- onSaved();
- }
-
- private void onSaved() {
- // AFTER SYNCHRONIZATION
- Log.d(LOG_TAG_AUTOSAVE, "FINISHED AUTOSAVE");
- saveActive = false;
-
- // AFTER "DELAY_AFTER_SYNC" SECONDS: allow next auto-save or start it directly
- handler.postDelayed(runAutoSave, DELAY_AFTER_SYNC);
-
- }
- });
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/NotePreviewFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/NotePreviewFragment.java
deleted file mode 100644
index b9f3f3246fba89a8de0c36a115de6e400e24409c..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/NotePreviewFragment.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package foundation.e.notes.android.fragment;
-
-import android.content.SharedPreferences;
-import android.graphics.Typeface;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.yydcdut.markdown.syntax.text.TextFactory;
-import com.yydcdut.rxmarkdown.RxMDTextView;
-import com.yydcdut.rxmarkdown.RxMarkdown;
-
-import androidx.annotation.Nullable;
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.util.MarkDownUtil;
-import rx.Subscriber;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
-
-public class NotePreviewFragment extends BaseNoteFragment {
-
- @BindView(R.id.single_note_content)
- RxMDTextView noteContent;
-
- public static NotePreviewFragment newInstance(long noteId) {
- NotePreviewFragment f = new NotePreviewFragment();
- Bundle b = new Bundle();
- b.putLong(PARAM_NOTE_ID, noteId);
- f.setArguments(b);
- return f;
- }
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
- menu.findItem(R.id.menu_edit).setVisible(true);
- menu.findItem(R.id.menu_preview).setVisible(false);
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.activity_single_note, container, false);
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- ButterKnife.bind(this, getView());
-
- setActiveTextView(noteContent);
-
- String content = note.getContent();
-
- RxMarkdown.with(content, getActivity())
- .config(
- MarkDownUtil.getMarkDownConfiguration(noteContent.getContext())
- /*.setOnTodoClickCallback(new OnTodoClickCallback() {
- @Override
- public CharSequence onTodoClicked(View view, String line, int lineNumber) {
- String[] lines = TextUtils.split(note.getContent(), "\\r?\\n");
- if(lines.length >= lineNumber) {
- lines[lineNumber] = line;
- }
- noteContent.setText(TextUtils.join("\n", lines), TextView.BufferType.SPANNABLE);
- saveNote(null);
- return line;
- }
- }
- )*/.build()
- )
- .factory(TextFactory.create())
- .intoObservable()
- .subscribeOn(Schedulers.computation())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(new Subscriber() {
- @Override
- public void onCompleted() {
- }
-
- @Override
- public void onError(Throwable e) {
- Log.v(getClass().getSimpleName(), "RxMarkdown error", e);
- }
-
- @Override
- public void onNext(CharSequence charSequence) {
- noteContent.setText(charSequence, TextView.BufferType.SPANNABLE);
- }
- });
- noteContent.setText(content);
- noteContent.setMovementMethod(LinkMovementMethod.getInstance());
-
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
- noteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(sp));
- if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
- noteContent.setTypeface(Typeface.MONOSPACE);
- }
- }
-
- @Override
- protected String getContent() {
- return note.getContent();
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/PreferencesFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/PreferencesFragment.java
deleted file mode 100644
index 31bdb431f632f0c7ba7db0cf903568ad3777f11d..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/PreferencesFragment.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package foundation.e.notes.android.fragment;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceFragment;
-import android.preference.SwitchPreference;
-import android.util.Log;
-import android.widget.Toast;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.notes.R;
-import foundation.e.notes.util.Notes;
-import androidx.annotation.Nullable;
-
-public class PreferencesFragment extends PreferenceFragment {
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- addPreferencesFromResource(R.xml.preferences);
-
- Preference resetTrust = findPreference(getString(R.string.pref_key_reset_trust));
- resetTrust.setOnPreferenceClickListener((Preference preference) -> {
- CustomCertManager.Companion.resetCertificates(getActivity());
- Toast.makeText(getActivity(), getString(R.string.settings_cert_reset_toast), Toast.LENGTH_SHORT).show();
- return true;
- });
-
- final SwitchPreference wifiOnlyPref = (SwitchPreference) findPreference(getString(R.string.pref_key_wifi_only));
- wifiOnlyPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
- Boolean syncOnWifiOnly = (Boolean) newValue;
- Log.v("Notes", "syncOnWifiOnly: " + syncOnWifiOnly);
- return true;
- });
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/fragment/about/AboutFragment.java b/app/src/main/java/foundation/e/notes/android/fragment/about/AboutFragment.java
deleted file mode 100644
index e6c13cbcce9f571eff2bf39d04701537398df924..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/fragment/about/AboutFragment.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package foundation.e.notes.android.fragment.about;
-
-import android.os.Bundle;
-import android.preference.PreferenceFragment;
-
-import androidx.annotation.Nullable;
-import foundation.e.notes.BuildConfig;
-import foundation.e.notes.R;
-
-public class AboutFragment extends PreferenceFragment {
-
- private static final String BUILD_VERSION = "build_version";
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- addPreferencesFromResource(R.xml.about_preferences);
-
- findPreference(BUILD_VERSION).setSummary(BuildConfig.VERSION_NAME);
-
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/android/providers/AppContentProvider.java b/app/src/main/java/foundation/e/notes/android/providers/AppContentProvider.java
deleted file mode 100644
index 30628bf0330f5296aa1490f30d80624bb1f359bb..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/android/providers/AppContentProvider.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package foundation.e.notes.android.providers;
-
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.net.Uri;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * @author Nihar Thakkar
- */
-
-public class AppContentProvider extends ContentProvider {
-
- @Override
- public boolean onCreate() {
- return false;
- }
-
- @Nullable
- @Override
- public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
- return null;
- }
-
- @Nullable
- @Override
- public String getType(@NonNull Uri uri) {
- return null;
- }
-
- @Nullable
- @Override
- public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
- return null;
- }
-
- @Override
- public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
- return 0;
- }
-
- @Override
- public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
- return 0;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/model/Category.java b/app/src/main/java/foundation/e/notes/model/Category.java
deleted file mode 100644
index 861c24027dcea32483678b27541a65b6c01f9799..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/Category.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package foundation.e.notes.model;
-
-import androidx.annotation.Nullable;
-
-import java.io.Serializable;
-
-public class Category implements Serializable {
-
- @Nullable
- public final String category;
- @Nullable
- public final Boolean favorite;
-
- public Category(@Nullable String category, @Nullable Boolean favorite) {
- this.category = category;
- this.favorite = favorite;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/model/CloudNote.java b/app/src/main/java/foundation/e/notes/model/CloudNote.java
deleted file mode 100644
index f218d0dd8521a1bf3b922ae17645897b4a00ba28..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/CloudNote.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package foundation.e.notes.model;
-
-import java.io.Serializable;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Locale;
-
-import foundation.e.notes.util.NoteUtil;
-
-/**
- * CloudNote represents a remote note from an OwnCloud server.
- * It can be directly generated from the JSON answer from the server.
- */
-public class CloudNote implements Serializable {
- private long remoteId = 0;
- private String title = "";
- private Calendar modified = null;
- private String content = "";
- private boolean favorite = false;
- private String category = "";
- private String etag = "";
-
- public CloudNote(long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag) {
- this.remoteId = remoteId;
- if (title != null)
- setTitle(title);
- setTitle(title);
- setContent(content);
- setFavorite(favorite);
- setCategory(category);
- setEtag(etag);
- this.modified = modified;
- }
-
- public long getRemoteId() {
- return remoteId;
- }
-
- public void setRemoteId(long remoteId) {
- this.remoteId = remoteId;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = NoteUtil.removeMarkDown(title);
- }
-
- @SuppressWarnings("WeakerAccess")
- public Calendar getModified() {
- return modified;
- }
-
- public String getModified(String format) {
- if (modified == null)
- return null;
- return new SimpleDateFormat(format, Locale.GERMANY).format(this.getModified().getTimeInMillis());
- }
-
- public void setModified(Calendar modified) {
- this.modified = modified;
- }
-
- public String getContent() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public boolean isFavorite() {
- return favorite;
- }
-
- public void setFavorite(boolean favorite) {
- this.favorite = favorite;
- }
-
- public String getEtag() {
- return etag;
- }
-
- public void setEtag(String etag) {
- this.etag = etag;
- }
-
- public String getCategory() {
- return category;
- }
-
- public void setCategory(String category) {
- this.category = category == null ? "" : category;
- }
-
- @Override
- public String toString() {
- final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
- return "R" + getRemoteId() + " " + (isFavorite() ? " (*) " : " ") + getCategory() + " / " + getTitle() + " (" + getModified(DATE_FORMAT) + " / " + getEtag() + ")";
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/notes/model/DBNote.java b/app/src/main/java/foundation/e/notes/model/DBNote.java
deleted file mode 100644
index 4681db55542644d66f8a2dd8d644283aa00ca37a..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/DBNote.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package foundation.e.notes.model;
-
-import java.io.Serializable;
-import java.util.Calendar;
-
-import foundation.e.notes.util.NoteUtil;
-
-/**
- * DBNote represents a single note from the local SQLite database with all attributes.
- * It extends CloudNote with attributes required for local data management.
- */
-public class DBNote extends CloudNote implements Item, Serializable {
-
- private long id;
- private DBStatus status;
- private String excerpt = "";
-
- public DBNote(long id, long remoteId, Calendar modified, String title, String content, boolean favorite, String category, String etag, DBStatus status) {
- super(remoteId, modified, title, content, favorite, category, etag);
- this.id = id;
- setExcerpt(content);
- this.status = status;
- }
-
- public long getId() {
- return id;
- }
-
- public DBStatus getStatus() {
- return status;
- }
-
- public void setStatus(DBStatus status) {
- this.status = status;
- }
-
- public String getExcerpt() {
- return excerpt;
- }
-
- public void setExcerptDirectly(String content) {
- excerpt = content;
- }
-
- private void setExcerpt(String content) {
- excerpt = NoteUtil.generateNoteExcerpt(content);
- }
-
- public void setContent(String content) {
- super.setContent(content);
- setExcerpt(content);
- }
-
- @Override
- public boolean isSection() {
- return false;
- }
-
- @Override
- public String toString() {
- return "#" + getId() + "/" + super.toString() + " " + getStatus();
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/model/Item.java b/app/src/main/java/foundation/e/notes/model/Item.java
deleted file mode 100644
index 20c6b29ec168b70a1aa2ecb7d2ac15cd8815b999..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/Item.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package foundation.e.notes.model;
-
-public interface Item {
- boolean isSection();
-}
diff --git a/app/src/main/java/foundation/e/notes/model/ItemAdapter.java b/app/src/main/java/foundation/e/notes/model/ItemAdapter.java
deleted file mode 100644
index 9bc79e12c26160fc4e1cce3b6f6391298883d1c7..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/ItemAdapter.java
+++ /dev/null
@@ -1,233 +0,0 @@
-package foundation.e.notes.model;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.Html;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-
-import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
-
-public class ItemAdapter extends RecyclerView.Adapter {
-
- private static final int section_type = 0;
- private static final int note_type = 1;
- private final NoteClickListener noteClickListener;
- private List- itemList = null;
- private boolean showCategory = true;
- private List selected = null;
-
- public ItemAdapter(@NonNull NoteClickListener noteClickListener) {
- this.itemList = new ArrayList<>();
- this.selected = new ArrayList<>();
- this.noteClickListener = noteClickListener;
- }
-
- /**
- * Updates the item list and notifies respective view to update.
- *
- * @param itemList List of items to be set
- */
- public void setItemList(@NonNull List
- itemList) {
- this.itemList = itemList;
- notifyDataSetChanged();
- }
-
- /**
- * Adds the given note to the top of the list.
- *
- * @param note Note that should be added.
- */
- public void add(@NonNull DBNote note) {
- itemList.add(0, note);
- notifyItemInserted(0);
- notifyItemChanged(0);
- }
-
- /**
- * Replaces a note with an updated version
- *
- * @param note Note with the changes.
- * @param position position in the list of the node
- */
- public void replace(@NonNull DBNote note, int position) {
- itemList.set(position, note);
- notifyItemChanged(position);
- }
-
- /**
- * Removes all items from the adapter.
- */
- public void removeAll() {
- itemList.clear();
- notifyDataSetChanged();
- }
-
- // Create new views (invoked by the layout manager)
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View v;
- if (viewType == section_type) {
- v = LayoutInflater.from(parent.getContext()).inflate(R.layout.fragment_notes_list_section_item, parent, false);
- return new SectionViewHolder(v);
- } else {
- v = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.fragment_notes_list_note_item, parent, false);
- return new NoteViewHolder(v);
- }
- }
-
- // Replace the contents of a view (invoked by the layout manager)
- @Override
- public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
- // - get element from your dataset at this position
- // - replace the contents of the view with that element
- Item item = itemList.get(position);
- if (item.isSection()) {
- SectionItem section = (SectionItem) item;
- ((SectionViewHolder) holder).sectionTitle.setText(section.geTitle());
- } else {
- final DBNote note = (DBNote) item;
- final NoteViewHolder nvHolder = ((NoteViewHolder) holder);
- nvHolder.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f);
- nvHolder.noteTitle.setText(Html.fromHtml(note.getTitle()));
- nvHolder.noteCategory.setVisibility(showCategory && !note.getCategory().isEmpty() ? View.VISIBLE : View.GONE);
- nvHolder.noteCategory.setText(Html.fromHtml(note.getCategory()));
- nvHolder.noteExcerpt.setText(Html.fromHtml(note.getExcerpt()));
- nvHolder.noteStatus.setVisibility(DBStatus.VOID.equals(note.getStatus()) ? View.INVISIBLE : View.VISIBLE);
- nvHolder.noteFavorite.setImageResource(note.isFavorite() ? lineageos.platform.R.drawable.ic_star_filled : lineageos.platform.R.drawable.ic_star);
- nvHolder.noteFavorite.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- noteClickListener.onNoteFavoriteClick(holder.getAdapterPosition(), view);
- }
- });
- }
- }
-
- public boolean select(Integer position) {
- return !selected.contains(position) && selected.add(position);
- }
-
- public void clearSelection() {
- selected.clear();
- }
-
- @NonNull
- public List getSelected() {
- return selected;
- }
-
- public boolean deselect(Integer position) {
- for (int i = 0; i < selected.size(); i++) {
- if (selected.get(i).equals(position)) {
- //position was selected and removed
- selected.remove(i);
- return true;
- }
- }
- // position was not selected
- return false;
- }
-
- public Item getItem(int notePosition) {
- return itemList.get(notePosition);
- }
-
- public void remove(@NonNull Item item) {
- itemList.remove(item);
- notifyDataSetChanged();
- }
-
- public void setShowCategory(boolean showCategory) {
- this.showCategory = showCategory;
- }
-
- @Override
- public int getItemCount() {
- return itemList.size();
- }
-
- @Override
- public int getItemViewType(int position) {
- return getItem(position).isSection() ? section_type : note_type;
- }
-
- public interface NoteClickListener {
- void onNoteClick(int position, View v);
-
- void onNoteFavoriteClick(int position, View v);
-
- boolean onNoteLongClick(int position, View v);
- }
-
- public class NoteViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener, View.OnClickListener {
- @BindView(R.id.noteSwipeable)
- public View noteSwipeable;
- View noteSwipeFrame;
- ImageView noteFavoriteLeft, noteDeleteRight;
- TextView noteTitle;
- @BindView(R.id.noteCategory)
- TextView noteCategory;
- @BindView(R.id.noteExcerpt)
- TextView noteExcerpt;
- @BindView(R.id.noteStatus)
- ImageView noteStatus;
- @BindView(R.id.noteFavorite)
- ImageView noteFavorite;
-
- private NoteViewHolder(View v) {
- super(v);
- this.noteSwipeFrame = v.findViewById(R.id.noteSwipeFrame);
- this.noteSwipeable = v.findViewById(R.id.noteSwipeable);
- this.noteFavoriteLeft = v.findViewById(R.id.noteFavoriteLeft);
- this.noteDeleteRight = v.findViewById(R.id.noteDeleteRight);
- this.noteTitle = v.findViewById(R.id.noteTitle);
- this.noteCategory = v.findViewById(R.id.noteCategory);
- this.noteExcerpt = v.findViewById(R.id.noteExcerpt);
- this.noteStatus = v.findViewById(R.id.noteStatus);
- this.noteFavorite = v.findViewById(R.id.noteFavorite);
- v.setOnClickListener(this);
- v.setOnLongClickListener(this);
- }
-
- @Override
- public void onClick(View v) {
- final int adapterPosition = getAdapterPosition();
- if (adapterPosition != NO_POSITION) {
- noteClickListener.onNoteClick(adapterPosition, v);
- }
- }
-
- @Override
- public boolean onLongClick(View v) {
- return noteClickListener.onNoteLongClick(getAdapterPosition(), v);
- }
-
- public void showSwipe(boolean left) {
- noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE);
- noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE);
- noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention);
- }
- }
-
- public static class SectionViewHolder extends RecyclerView.ViewHolder {
- @BindView(R.id.sectionTitle)
- TextView sectionTitle;
-
- private SectionViewHolder(View view) {
- super(view);
- ButterKnife.bind(this, view);
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/notes/model/NavigationAdapter.java b/app/src/main/java/foundation/e/notes/model/NavigationAdapter.java
deleted file mode 100644
index a652e49470967e6732f1c8c9a78fc42cde47b115..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/NavigationAdapter.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package foundation.e.notes.model;
-
-import android.graphics.Color;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.DrawableRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.ColorUtils;
-import androidx.recyclerview.widget.RecyclerView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import butterknife.BindView;
-import butterknife.ButterKnife;
-import foundation.e.notes.R;
-import foundation.e.notes.util.NoteUtil;
-
-public class NavigationAdapter extends RecyclerView.Adapter {
-
- @DrawableRes
- public static final int ICON_FOLDER = lineageos.platform.R.drawable.ic_folder;
- @DrawableRes
- public static final int ICON_NOFOLDER = R.drawable.ic_folder_open_grey600_24dp;
- @DrawableRes
- public static final int ICON_SUB_FOLDER = R.drawable.ic_folder_grey600_18dp;
- @DrawableRes
- public static final int ICON_MULTIPLE = R.drawable.ic_create_new_folder_grey600_24dp;
- @DrawableRes
- public static final int ICON_MULTIPLE_OPEN = R.drawable.ic_folder_grey600_24dp;
- @DrawableRes
- public static final int ICON_SUB_MULTIPLE = R.drawable.ic_create_new_folder_grey600_18dp;
-
- public static class NavigationItem {
- @NonNull
- public String id;
- @NonNull
- public String label;
- @DrawableRes
- public int icon;
- @Nullable
- public Integer count;
-
- public NavigationItem(@NonNull String id, @NonNull String label, @Nullable Integer count, @DrawableRes int icon) {
- this.id = id;
- this.label = label;
- this.count = count;
- this.icon = icon;
- }
- }
-
- class ViewHolder extends RecyclerView.ViewHolder {
- @NonNull
- private final View view;
-
- @BindView(R.id.navigationItemLabel)
- TextView name;
-
- @BindView(R.id.navigationItemCount)
- TextView count;
-
- @BindView(R.id.navigationItemIcon)
- ImageView icon;
-
- private NavigationItem currentItem;
-
- ViewHolder(@NonNull View itemView, @NonNull final ClickListener clickListener) {
- super(itemView);
- view = itemView;
- ButterKnife.bind(this, view);
- icon.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- clickListener.onIconClick(currentItem);
- }
- });
- itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- clickListener.onItemClick(currentItem);
- }
- });
- }
-
- void assignItem(@NonNull NavigationItem item) {
- currentItem = item;
- boolean isSelected = item.id.equals(selectedItem);
- name.setText(NoteUtil.extendCategory(item.label));
- count.setVisibility(item.count == null ? View.GONE : View.VISIBLE);
- count.setText(String.valueOf(item.count));
- if (item.icon > 0) {
- icon.setImageDrawable(ContextCompat.getDrawable(icon.getContext(), item.icon));
- icon.setVisibility(View.VISIBLE);
- } else {
- icon.setVisibility(View.GONE);
- }
- view.setBackgroundColor(isSelected ? ColorUtils.setAlphaComponent(view.getResources().getColor(R.color.accent_color), 20) : Color.TRANSPARENT);
- int textColor = ContextCompat.getColor(view.getContext(), isSelected ? R.color.accent_color : R.color.color_default_primary_text);
- int unSelectedIconColor = ContextCompat.getColor(view.getContext(), isSelected ? R.color.accent_color : R.color.drawer_menu_icon_color);
-
- name.setTextColor(textColor);
- count.setTextColor(textColor);
- icon.setColorFilter(isSelected ? textColor : unSelectedIconColor);
- }
- }
-
-// private Drawable reSizeIcon(Resources resources, Drawable drawable){
-// Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
-// Drawable resizeDrawable = new BitmapDrawable(resources, Bitmap.createScaledBitmap(bitmap, 24, 24, true));
-// return resizeDrawable;
-// }
-
- public interface ClickListener {
- void onItemClick(NavigationItem item);
-
- void onIconClick(NavigationItem item);
- }
-
- @NonNull
- private List items = new ArrayList<>();
- private String selectedItem = null;
- @NonNull
- private ClickListener clickListener;
-
- public NavigationAdapter(@NonNull ClickListener clickListener) {
- this.clickListener = clickListener;
- }
-
- @NonNull
- @Override
- public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false);
- return new ViewHolder(v, clickListener);
- }
-
- @Override
- public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
- holder.assignItem(items.get(position));
- }
-
- @Override
- public int getItemCount() {
- return items.size();
- }
-
- public void setItems(@NonNull List items) {
- this.items = items;
- notifyDataSetChanged();
- }
-
- public void setSelectedItem(String id) {
- selectedItem = id;
- notifyDataSetChanged();
- }
-
- public String getSelectedItem() {
- return selectedItem;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/model/SectionItem.java b/app/src/main/java/foundation/e/notes/model/SectionItem.java
deleted file mode 100644
index 8bab706e6ee224b620b6567385c4e6c80767c5aa..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/model/SectionItem.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package foundation.e.notes.model;
-
-public class SectionItem implements Item {
-
- private String title;
-
- public SectionItem(String title) {
- this.title = title;
- }
-
- public String geTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- @Override
- public boolean isSection() {
- return true;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/persistence/LoadNotesListTask.java b/app/src/main/java/foundation/e/notes/persistence/LoadNotesListTask.java
deleted file mode 100644
index b0a960892d2e1b65b6342148a49834e88bf94fd3..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/persistence/LoadNotesListTask.java
+++ /dev/null
@@ -1,172 +0,0 @@
-package foundation.e.notes.persistence;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.text.Html;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.text.style.ForegroundColorSpan;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.core.content.ContextCompat;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import foundation.e.notes.R;
-import foundation.e.notes.model.Category;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.model.Item;
-import foundation.e.notes.model.SectionItem;
-import foundation.e.notes.util.NoteUtil;
-
-public class LoadNotesListTask extends AsyncTask> {
-
- private final Context context;
- private final NotesLoadedListener callback;
- private final Category category;
- private final CharSequence searchQuery;
-
- public LoadNotesListTask(@NonNull Context context, @NonNull NotesLoadedListener callback, @NonNull Category category, @Nullable CharSequence searchQuery) {
- this.context = context;
- this.callback = callback;
- this.category = category;
- this.searchQuery = searchQuery;
- }
-
- @Override
- protected List
- doInBackground(Void... voids) {
- List noteList;
- NoteSQLiteOpenHelper db = NoteSQLiteOpenHelper.getInstance(context);
- noteList = db.searchNotes(searchQuery, category.category, category.favorite);
-
- if (category.category == null) {
- return fillListByTime(noteList);
- } else {
- return fillListByCategory(noteList);
- }
- }
-
- private DBNote colorTheNote(DBNote dbNote) {
- if (!TextUtils.isEmpty(searchQuery)) {
- SpannableString spannableString = new SpannableString(dbNote.getTitle());
- Matcher matcher = Pattern.compile("(" + searchQuery + ")", Pattern.CASE_INSENSITIVE).matcher(spannableString);
- while (matcher.find()) {
- spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, foundation.e.notes.R.color.light_grey)),
- matcher.start(), matcher.end(), 0);
- }
-
- dbNote.setTitle(Html.toHtml(spannableString));
-
- spannableString = new SpannableString(dbNote.getExcerpt());
- matcher = Pattern.compile("(" + searchQuery + ")", Pattern.CASE_INSENSITIVE).matcher(spannableString);
- while (matcher.find()) {
- spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, foundation.e.notes.R.color.light_grey)),
- matcher.start(), matcher.end(), 0);
- }
-
- dbNote.setExcerptDirectly(Html.toHtml(spannableString));
- }
-
- return dbNote;
- }
-
- @NonNull
- @WorkerThread
- private List
- fillListByCategory(@NonNull List noteList) {
- List
- itemList = new ArrayList<>();
- String currentCategory = category.category;
- for (DBNote note : noteList) {
- if (currentCategory != null && !currentCategory.equals(note.getCategory())) {
- itemList.add(new SectionItem(NoteUtil.extendCategory(note.getCategory())));
- }
-
- itemList.add(colorTheNote(note));
- currentCategory = note.getCategory();
- }
- return itemList;
- }
-
- @NonNull
- @WorkerThread
- private List
- fillListByTime(@NonNull List noteList) {
- List
- itemList = new ArrayList<>();
- Timeslotter timeslotter = new Timeslotter();
- String lastTimeslot = null;
- for (int i = 0; i < noteList.size(); i++) {
- DBNote currentNote = noteList.get(i);
- String timeslot = timeslotter.getTimeslot(currentNote);
- if(i > 0 && !timeslot.equals(lastTimeslot)) {
- itemList.add(new SectionItem(timeslot));
- }
- itemList.add(colorTheNote(currentNote));
- lastTimeslot = timeslot;
- }
-
- return itemList;
- }
-
- @Override
- protected void onPostExecute(List
- items) {
- callback.onNotesLoaded(items, category.category == null);
- }
-
- public interface NotesLoadedListener {
- void onNotesLoaded(List
- notes, boolean showCategory);
- }
-
- private class Timeslotter {
- private final List timeslots = new ArrayList<>();
- private final Calendar lastYear;
-
- Timeslotter() {
- Calendar now = Calendar.getInstance();
- int month = now.get(Calendar.MONTH);
- int day = now.get(Calendar.DAY_OF_MONTH);
- int offsetWeekStart = (now.get(Calendar.DAY_OF_WEEK) - now.getFirstDayOfWeek() + 7) % 7;
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_today), month, day));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_yesterday), month,day - 1));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_week), month,day - offsetWeekStart));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_week), month,day - offsetWeekStart - 7));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_this_month), month,1));
- timeslots.add(new Timeslot(context.getResources().getString(R.string.listview_updated_last_month), month - 1, 1));
- lastYear = Calendar.getInstance();
- lastYear.set(now.get(Calendar.YEAR) - 1, 0, 1, 0, 0, 0);
- }
-
- String getTimeslot(DBNote note) {
- if (note.isFavorite()) {
- return "";
- }
- Calendar modified = note.getModified();
- for (Timeslot timeslot : timeslots) {
- if (!modified.before(timeslot.time)) {
- return timeslot.label;
- }
- }
- if (!modified.before(this.lastYear)) {
- // use YEAR and MONTH in a format based on current locale
- return DateUtils.formatDateTime(context, modified.getTimeInMillis(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NO_MONTH_DAY);
- } else {
- return Integer.toString(modified.get(Calendar.YEAR));
- }
- }
-
- private class Timeslot {
- final String label;
- final Calendar time;
-
- Timeslot(String label, int month, int day) {
- this.label = label;
- this.time = Calendar.getInstance();
- this.time.set(this.time.get(Calendar.YEAR), month, day, 0, 0, 0);
- }
- }
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/persistence/NoteSQLiteOpenHelper.java b/app/src/main/java/foundation/e/notes/persistence/NoteSQLiteOpenHelper.java
deleted file mode 100644
index 210705189db47a9fe75eea31b767a7f96532c813..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/persistence/NoteSQLiteOpenHelper.java
+++ /dev/null
@@ -1,599 +0,0 @@
-package foundation.e.notes.persistence;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutManager;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.model.DBStatus;
-import foundation.e.notes.model.NavigationAdapter;
-import foundation.e.notes.util.ICallback;
-import foundation.e.notes.util.NoteUtil;
-import foundation.e.notes.android.appwidget.NoteListWidget;
-import foundation.e.notes.android.appwidget.SingleNoteWidget;
-import foundation.e.notes.R;
-
-/**
- * Helps to add, get, update and delete Notes with the option to trigger a Resync with the Server.
- */
-public class NoteSQLiteOpenHelper extends SQLiteOpenHelper {
-
- private static final int database_version = 8;
- private static final String database_name = "OWNCLOUD_NOTES";
- private static final String table_notes = "NOTES";
- private static final String key_id = "ID";
- private static final String key_remote_id = "REMOTEID";
- private static final String key_status = "STATUS";
- private static final String key_title = "TITLE";
- private static final String key_modified = "MODIFIED";
- private static final String key_content = "CONTENT";
- private static final String key_favorite = "FAVORITE";
- private static final String key_category = "CATEGORY";
- private static final String key_etag = "ETAG";
- private static final String[] columns = {key_id, key_remote_id, key_status, key_title, key_modified, key_content, key_favorite, key_category, key_etag};
- private static final String default_order = key_favorite + " DESC, " + key_modified + " DESC";
-
- private static NoteSQLiteOpenHelper instance;
-
- private NoteServerSyncHelper serverSyncHelper;
- private Context context;
-
- private NoteSQLiteOpenHelper(Context context) {
- super(context, database_name, null, database_version);
- this.context = context.getApplicationContext();
- serverSyncHelper = NoteServerSyncHelper.getInstance(this);
- }
-
- public static NoteSQLiteOpenHelper getInstance(Context context) {
- if (instance == null)
- return instance = new NoteSQLiteOpenHelper(context.getApplicationContext());
- else
- return instance;
- }
-
- public NoteServerSyncHelper getNoteServerSyncHelper() {
- return serverSyncHelper;
- }
-
- /**
- * Creates initial the Database
- *
- * @param db Database
- */
- @Override
- public void onCreate(SQLiteDatabase db) {
- createTable(db, table_notes);
- }
-
- private void createTable(SQLiteDatabase db, String tableName) {
- db.execSQL("CREATE TABLE " + tableName + " ( " +
- key_id + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
- key_remote_id + " INTEGER, " +
- key_status + " VARCHAR(50), " +
- key_title + " TEXT, " +
- key_modified + " INTEGER DEFAULT 0, " +
- key_content + " TEXT, " +
- key_favorite + " INTEGER DEFAULT 0, " +
- key_category + " TEXT NOT NULL DEFAULT '', " +
- key_etag + " TEXT)");
- createIndexes(db);
- }
-
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (oldVersion < 3) {
- recreateDatabase(db);
- }
- if (oldVersion < 4) {
- clearDatabase(db);
- }
- if (oldVersion < 5) {
- db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_remote_id + " INTEGER");
- db.execSQL("UPDATE " + table_notes + " SET " + key_remote_id + "=" + key_id + " WHERE (" + key_remote_id + " IS NULL OR " + key_remote_id + "=0) AND " + key_status + "!=?", new String[]{DBStatus.LOCAL_CREATED.getTitle()});
- db.execSQL("UPDATE " + table_notes + " SET " + key_remote_id + "=0, " + key_status + "=? WHERE " + key_status + "=?", new String[]{DBStatus.LOCAL_EDITED.getTitle(), DBStatus.LOCAL_CREATED.getTitle()});
- }
- if (oldVersion < 6) {
- db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_favorite + " INTEGER DEFAULT 0");
- }
- if (oldVersion < 7) {
- dropIndexes(db);
- db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_category + " TEXT NOT NULL DEFAULT ''");
- db.execSQL("ALTER TABLE " + table_notes + " ADD COLUMN " + key_etag + " TEXT");
- createIndexes(db);
- }
- if (oldVersion < 8) {
- final String table_temp = "NOTES_TEMP";
- createTable(db, table_temp);
- db.execSQL(String.format("INSERT INTO %s(%s,%s,%s,%s,%s,%s,%s,%s,%s) ", table_temp, key_id, key_remote_id, key_status, key_title, key_modified, key_content, key_favorite, key_category, key_etag)
- + String.format("SELECT %s,%s,%s,%s,strftime('%%s',%s),%s,%s,%s,%s FROM %s", key_id, key_remote_id, key_status, key_title, key_modified, key_content, key_favorite, key_category, key_etag, table_notes));
- db.execSQL(String.format("DROP TABLE %s", table_notes));
- db.execSQL(String.format("ALTER TABLE %s RENAME TO %s", table_temp, table_notes));
- }
- }
-
- @Override
- public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- recreateDatabase(db);
- }
-
- private void clearDatabase(SQLiteDatabase db) {
- db.delete(table_notes, null, null);
- }
-
- private void recreateDatabase(SQLiteDatabase db) {
- dropIndexes(db);
- db.execSQL("DROP TABLE " + table_notes);
- onCreate(db);
- }
-
- private void dropIndexes(SQLiteDatabase db) {
- Cursor c = db.query("sqlite_master", new String[]{"name"}, "type=?", new String[]{"index"}, null, null, null);
- while (c.moveToNext()) {
- db.execSQL("DROP INDEX " + c.getString(0));
- }
- c.close();
- }
-
- private void createIndexes(SQLiteDatabase db) {
- createIndex(db, table_notes, key_remote_id);
- createIndex(db, table_notes, key_status);
- createIndex(db, table_notes, key_favorite);
- createIndex(db, table_notes, key_category);
- createIndex(db, table_notes, key_modified);
- }
-
- private void createIndex(SQLiteDatabase db, String table, String column) {
- String indexName = table + "_" + column + "_idx";
- db.execSQL("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + table + "(" + column + ")");
- }
-
- public Context getContext() {
- return context;
- }
-
- /**
- * Creates a new Note in the Database and adds a Synchronization Flag.
- *
- * @param content String
- */
- @SuppressWarnings("UnusedReturnValue")
- public long addNoteAndSync(String content, String category, boolean favorite) {
- CloudNote note = new CloudNote(0, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, getContext()), content, favorite, category, null);
- return addNoteAndSync(note);
- }
-
- /**
- * Creates a new Note in the Database and adds a Synchronization Flag.
- *
- * @param note Note
- */
- @SuppressWarnings("UnusedReturnValue")
- public long addNoteAndSync(CloudNote note) {
- DBNote dbNote = new DBNote(0, 0, note.getModified(), note.getTitle(), note.getContent(), note.isFavorite(), note.getCategory(), note.getEtag(), DBStatus.LOCAL_EDITED);
- long id = addNote(dbNote);
- notifyNotesChanged();
- getNoteServerSyncHelper().scheduleSync(true);
- return id;
- }
-
- /**
- * Inserts a note directly into the Database.
- * No Synchronisation will be triggered! Use addNoteAndSync()!
- *
- * @param note Note to be added. Remotely created Notes must be of type CloudNote and locally created Notes must be of Type DBNote (with DBStatus.LOCAL_EDITED)!
- */
- long addNote(CloudNote note) {
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues();
- if (note instanceof DBNote) {
- DBNote dbNote = (DBNote) note;
- if (dbNote.getId() > 0) {
- values.put(key_id, dbNote.getId());
- }
- values.put(key_status, dbNote.getStatus().getTitle());
- } else {
- values.put(key_status, DBStatus.VOID.getTitle());
- }
- if (note.getRemoteId() > 0) {
- values.put(key_remote_id, note.getRemoteId());
- }
- values.put(key_title, note.getTitle());
- values.put(key_modified, note.getModified().getTimeInMillis() / 1000);
- values.put(key_content, note.getContent());
- values.put(key_favorite, note.isFavorite());
- values.put(key_category, note.getCategory());
- values.put(key_etag, note.getEtag());
- return db.insert(table_notes, null, values);
- }
-
- /**
- * Get a single Note by ID
- *
- * @param id int - ID of the requested Note
- * @return requested Note
- */
- public DBNote getNote(long id) {
- List notes = getNotesCustom(key_id + " = ? AND " + key_status + " != ?", new String[]{String.valueOf(id), DBStatus.LOCAL_DELETED.getTitle()}, null);
- return notes.isEmpty() ? null : notes.get(0);
- }
-
- /**
- * Query the database with a custom raw query.
- *
- * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself).
- * @param selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in order that they appear in the selection. The values will be bound as Strings.
- * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.
- * @return List of Notes
- */
- @NonNull
- @WorkerThread
- private List getNotesCustom(@NonNull String selection, @NonNull String[] selectionArgs, @Nullable String orderBy) {
- return this.getNotesCustom(selection, selectionArgs, orderBy, null);
- }
-
- @NonNull
- @WorkerThread
- private List getNotesCustom(@NonNull String selection, @NonNull String[] selectionArgs, @Nullable String orderBy, @Nullable String limit) {
- SQLiteDatabase db = getReadableDatabase();
- if (selectionArgs.length > 2) {
- Log.v("Note", selection + " ---- " + selectionArgs[0] + " " + selectionArgs[1] + " " + selectionArgs[2]);
- }
- Cursor cursor = db.query(table_notes, columns, selection, selectionArgs, null, null, orderBy, limit);
- List notes = new ArrayList<>();
- while (cursor.moveToNext()) {
- notes.add(getNoteFromCursor(cursor));
- }
- cursor.close();
- return notes;
- }
-
- /**
- * Creates a DBNote object from the current row of a Cursor.
- *
- * @param cursor database cursor
- * @return DBNote
- */
- @NonNull
- private DBNote getNoteFromCursor(@NonNull Cursor cursor) {
- Calendar modified = Calendar.getInstance();
- modified.setTimeInMillis(cursor.getLong(4) * 1000);
- return new DBNote(cursor.getLong(0), cursor.getLong(1), modified, cursor.getString(3), cursor.getString(5), cursor.getInt(6) > 0, cursor.getString(7), cursor.getString(8), DBStatus.parse(cursor.getString(2)));
- }
-
- public void debugPrintFullDB() {
- List notes = getNotesCustom("", new String[]{}, default_order);
- Log.v(getClass().getSimpleName(), "Full Database (" + notes.size() + " notes):");
- for (DBNote note : notes) {
- Log.v(getClass().getSimpleName(), " " + note);
- }
- }
-
- @NonNull
- @WorkerThread
- public Map getIdMap() {
- Map result = new HashMap<>();
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(table_notes, new String[]{key_remote_id, key_id}, key_status + " != ?", new String[]{DBStatus.LOCAL_DELETED.getTitle()}, null, null, null);
- while (cursor.moveToNext()) {
- result.put(cursor.getLong(0), cursor.getLong(1));
- }
- cursor.close();
- return result;
- }
-
- /**
- * Returns a list of all Notes in the Database
- *
- * @return List<Note>
- */
- @NonNull
- @WorkerThread
- public List getNotes() {
- return getNotesCustom(key_status + " != ?", new String[]{DBStatus.LOCAL_DELETED.getTitle()}, default_order);
- }
-
- @NonNull
- @WorkerThread
- public List getRecentNotes() {
- return getNotesCustom(key_status + " != ?", new String[]{DBStatus.LOCAL_DELETED.getTitle()}, key_modified + " DESC", "4");
- }
-
- /**
- * Returns a list of all Notes in the Database
- *
- * @return List<Note>
- */
- @NonNull
- @WorkerThread
- public List searchNotes(@Nullable CharSequence query, @Nullable String category, @Nullable Boolean favorite) {
- List where = new ArrayList<>();
- List args = new ArrayList<>();
-
- where.add(key_status + " != ?");
- args.add(DBStatus.LOCAL_DELETED.getTitle());
-
- if (query != null) {
- where.add(key_status + " != ?");
- args.add(DBStatus.LOCAL_DELETED.getTitle());
-
- where.add("(" + key_title + " LIKE ? OR " + key_content + " LIKE ? OR " + key_category + " LIKE ?" + ")");
- args.add("%" + query + "%");
- args.add("%" + query + "%");
- args.add("%" + query + "%");
- }
-
- if (category != null) {
- where.add("(" + key_category + "=? OR " + key_category + " LIKE ? )");
- args.add(category);
- args.add(category + "/%");
- }
-
- if (favorite != null) {
- where.add(key_favorite + "=?");
- args.add(favorite ? "1" : "0");
- }
-
- String order = category == null ? default_order : key_category + ", " + key_title;
- return getNotesCustom(TextUtils.join(" AND ", where), args.toArray(new String[]{}), order);
- }
-
- /**
- * Returns a list of all Notes in the Database with were modified locally
- *
- * @return List<Note>
- */
- @NonNull
- @WorkerThread
- public List getLocalModifiedNotes() {
- return getNotesCustom(key_status + " != ?", new String[]{DBStatus.VOID.getTitle()}, null);
- }
-
- @NonNull
- @WorkerThread
- public Map getFavoritesCount() {
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(
- table_notes,
- new String[]{key_favorite, "COUNT(*)"},
- key_status + " != ?",
- new String[]{DBStatus.LOCAL_DELETED.getTitle()},
- key_favorite,
- null,
- key_favorite);
- Map favorites = new HashMap<>(cursor.getCount());
- while (cursor.moveToNext()) {
- favorites.put(cursor.getString(0), cursor.getInt(1));
- }
- cursor.close();
- return favorites;
- }
-
- @NonNull
- @WorkerThread
- public List getCategories() {
- SQLiteDatabase db = getReadableDatabase();
- Cursor cursor = db.query(
- table_notes,
- new String[]{key_category, "COUNT(*)"},
- key_status + " != ?",
- new String[]{DBStatus.LOCAL_DELETED.getTitle()},
- key_category,
- null,
- key_category);
- List categories = new ArrayList<>(cursor.getCount());
- while (cursor.moveToNext()) {
- categories.add(new NavigationAdapter.NavigationItem("category:" + cursor.getString(0), cursor.getString(0), cursor.getInt(1), NavigationAdapter.ICON_FOLDER));
- }
- cursor.close();
- return categories;
- }
-
- public void toggleFavorite(@NonNull DBNote note, @Nullable ICallback callback) {
- note.setFavorite(!note.isFavorite());
- note.setStatus(DBStatus.LOCAL_EDITED);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put(key_status, note.getStatus().getTitle());
- values.put(key_favorite, note.isFavorite() ? "1" : "0");
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(note.getId())});
- if (callback != null) {
- serverSyncHelper.addCallbackPush(callback);
- }
- serverSyncHelper.scheduleSync(true);
- }
-
- public void setCategory(@NonNull DBNote note, @NonNull String category, @Nullable ICallback callback) {
- note.setCategory(category);
- note.setStatus(DBStatus.LOCAL_EDITED);
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put(key_status, note.getStatus().getTitle());
- values.put(key_category, note.getCategory());
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(note.getId())});
- if (callback != null) {
- serverSyncHelper.addCallbackPush(callback);
- }
- serverSyncHelper.scheduleSync(true);
- }
-
- /**
- * Updates a single Note with a new content.
- * The title is derived from the new content automatically, and modified date as well as DBStatus are updated, too -- if the content differs to the state in the database.
- *
- * @param oldNote Note to be changed
- * @param newContent New content. If this is
null
, then oldNote
is saved again (useful for undoing changes).
- * @param callback When the synchronization is finished, this callback will be invoked (optional).
- * @return changed note if differs from database, otherwise the old note.
- */
- public DBNote updateNoteAndSync(@NonNull DBNote oldNote, @Nullable String newContent, @Nullable ICallback callback) {
- //debugPrintFullDB();
- DBNote newNote;
- if (newContent == null) {
- newNote = new DBNote(oldNote.getId(), oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.isFavorite(), oldNote.getCategory(), oldNote.getEtag(), DBStatus.LOCAL_EDITED);
- } else {
- newNote = new DBNote(oldNote.getId(), oldNote.getRemoteId(), Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(newContent, getContext()), newContent, oldNote.isFavorite(), oldNote.getCategory(), oldNote.getEtag(), DBStatus.LOCAL_EDITED);
- }
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put(key_status, newNote.getStatus().getTitle());
- values.put(key_title, newNote.getTitle());
- values.put(key_category, newNote.getCategory());
- values.put(key_modified, newNote.getModified().getTimeInMillis() / 1000);
- values.put(key_content, newNote.getContent());
- int rows = db.update(table_notes, values, key_id + " = ? AND (" + key_content + " != ? OR " + key_category + " != ?)", new String[]{String.valueOf(newNote.getId()), newNote.getContent(), newNote.getCategory()});
- // if data was changed, set new status and schedule sync (with callback); otherwise invoke callback directly.
- if (rows > 0) {
- notifyNotesChanged();
- if (callback != null) {
- serverSyncHelper.addCallbackPush(callback);
- }
- serverSyncHelper.scheduleSync(true);
- return newNote;
- } else {
- if (callback != null) {
- callback.onFinish();
- }
- return oldNote;
- }
- }
-
- /**
- * Updates a single Note with data from the server, (if it was not modified locally).
- * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization.
- * This is used by the synchronization task, hence no Synchronization will be triggered. Use updateNoteAndSync() instead!
- *
- * @param id local ID of Note
- * @param remoteNote Note from the server.
- * @param forceUnchangedDBNoteState is not null, then the local note is updated only if it was not modified meanwhile
- * @return The number of the Rows affected.
- */
- int updateNote(long id, @NonNull CloudNote remoteNote, @Nullable DBNote forceUnchangedDBNoteState) {
- SQLiteDatabase db = this.getWritableDatabase();
-
- // First, update the remote ID, since this field cannot be changed in parallel, but have to be updated always.
- ContentValues values = new ContentValues();
- values.put(key_remote_id, remoteNote.getRemoteId());
- db.update(table_notes, values, key_id + " = ?", new String[]{String.valueOf(id)});
-
- // The other columns have to be updated in dependency of forceUnchangedDBNoteState,
- // since the Synchronization-Task must not overwrite locales changes!
- values.clear();
- values.put(key_status, DBStatus.VOID.getTitle());
- values.put(key_title, remoteNote.getTitle());
- values.put(key_modified, remoteNote.getModified().getTimeInMillis() / 1000);
- values.put(key_content, remoteNote.getContent());
- values.put(key_favorite, remoteNote.isFavorite());
- values.put(key_category, remoteNote.getCategory());
- values.put(key_etag, remoteNote.getEtag());
- String whereClause;
- String[] whereArgs;
- if (forceUnchangedDBNoteState != null) {
- // used by: NoteServerSyncHelper.SyncTask.pushLocalChanges()
- // update only, if not modified locally during the synchronization
- // (i.e. all (!) user changeable columns (content, favorite) should still have the same value),
- // uses reference value gathered at start of synchronization
- whereClause = key_id + " = ? AND " + key_content + " = ? AND " + key_favorite + " = ? AND " + key_category + " = ?";
- whereArgs = new String[]{String.valueOf(id), forceUnchangedDBNoteState.getContent(), forceUnchangedDBNoteState.isFavorite() ? "1" : "0", forceUnchangedDBNoteState.getCategory()};
- } else {
- // used by: NoteServerSyncHelper.SyncTask.pullRemoteChanges()
- // update only, if not modified locally (i.e. STATUS="") and if modified remotely (i.e. any (!) column has changed)
- whereClause = key_id + " = ? AND " + key_status + " = ? AND (" + key_modified + "!=? OR " + key_title + "!=? OR " + key_favorite + "!=? OR " + key_category + "!=? OR " + (remoteNote.getEtag() != null ? key_etag + " IS NULL OR " : "") + key_etag + "!=? OR " + key_content + "!=?)";
- whereArgs = new String[]{String.valueOf(id), DBStatus.VOID.getTitle(), Long.toString(remoteNote.getModified().getTimeInMillis() / 1000), remoteNote.getTitle(), remoteNote.isFavorite() ? "1" : "0", remoteNote.getCategory(), remoteNote.getEtag(), remoteNote.getContent()};
- }
- int i = db.update(table_notes, values, whereClause, whereArgs);
- Log.d(getClass().getSimpleName(), "updateNote: " + remoteNote + " || forceUnchangedDBNoteState: " + forceUnchangedDBNoteState + " => " + i + " rows updated");
- return i;
- }
-
- /**
- * Marks a Note in the Database as Deleted. In the next Synchronization it will be deleted
- * from the Server.
- *
- * @param id long - ID of the Note that should be deleted
- * @return Affected rows
- */
- @SuppressWarnings("UnusedReturnValue")
- public int deleteNoteAndSync(long id) {
- SQLiteDatabase db = this.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put(key_status, DBStatus.LOCAL_DELETED.getTitle());
- int i = db.update(table_notes,
- values,
- key_id + " = ?",
- new String[]{String.valueOf(id)});
- notifyNotesChanged();
- getNoteServerSyncHelper().scheduleSync(true);
-
- if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
- shortcutManager.getPinnedShortcuts().forEach((shortcut) -> {
- String shortcutId = id + "";
- if (shortcut.getId().equals(shortcutId)) {
- Log.v(NoteSQLiteOpenHelper.class.getSimpleName(), "Removing shortcut for " + shortcutId);
- shortcutManager.disableShortcuts(Collections.singletonList(shortcutId), context.getResources().getString(R.string.note_has_been_deleted));
- }
- });
- }
- return i;
- }
-
- /**
- * Delete a single Note from the Database, if it has a specific DBStatus.
- * Thereby, an optimistic concurrency control is realized in order to prevent conflicts arising due to parallel changes from the UI and synchronization.
- *
- * @param id long - ID of the Note that should be deleted.
- * @param forceDBStatus DBStatus, e.g., if Note was marked as LOCAL_DELETED (for NoteSQLiteOpenHelper.SyncTask.pushLocalChanges()) or is unchanged VOID (for NoteSQLiteOpenHelper.SyncTask.pullRemoteChanges())
- */
- void deleteNote(long id, @NonNull DBStatus forceDBStatus) {
- SQLiteDatabase db = this.getWritableDatabase();
- db.delete(table_notes,
- key_id + " = ? AND " + key_status + " = ?",
- new String[]{String.valueOf(id), forceDBStatus.getTitle()});
- }
-
- /**
- * Notify about changed notes.
- */
- void notifyNotesChanged() {
- updateSingleNoteWidgets();
- updateNoteListWidgets();
- }
-
- /**
- * Update single note widget, if the note data was changed.
- */
- private void updateSingleNoteWidgets() {
- Intent intent = new Intent(getContext(), SingleNoteWidget.class);
- intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
- getContext().sendBroadcast(intent);
- }
-
- /**
- * Update note list widgets, if the note data was changed.
- */
- private void updateNoteListWidgets() {
- Intent intent = new Intent(getContext(), NoteListWidget.class);
- intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
- getContext().sendBroadcast(intent);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/persistence/NoteServerSyncHelper.java b/app/src/main/java/foundation/e/notes/persistence/NoteServerSyncHelper.java
deleted file mode 100644
index e66f91a3ffa2a58069c46d3614134b744b42d5e7..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/persistence/NoteServerSyncHelper.java
+++ /dev/null
@@ -1,499 +0,0 @@
-package foundation.e.notes.persistence;
-
-import android.accounts.AccountManager;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.preference.PreferenceManager;
-import android.util.Log;
-import android.widget.Toast;
-
-import org.json.JSONException;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.cert4android.CustomCertService;
-import foundation.e.cert4android.ICustomCertService;
-import foundation.e.cert4android.IOnCertificateDecision;
-import foundation.e.notes.android.activity.SettingsActivity;
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.model.DBNote;
-import foundation.e.notes.model.DBStatus;
-import foundation.e.notes.util.ICallback;
-import foundation.e.notes.util.NotesClient;
-import foundation.e.notes.util.NotesClientUtil;
-import foundation.e.notes.util.ServerResponse;
-import foundation.e.notes.util.SupportUtil;
-import foundation.e.notes.R;
-
-/**
- * @author Nihar Thakkar
- *
- * Helps to synchronize the Database to the Server.
- */
-public class NoteServerSyncHelper {
-
- private static NoteServerSyncHelper instance;
-
- private static final String eelo_account_type = "e.foundation.webdav.eelo";
- private static final String account_email_address_key = "email_address";
- private static final String notes_content_authority = "foundation.e.notes.android.providers.AppContentProvider";
-
- /**
- * Get (or create) instance from NoteServerSyncHelper.
- * This has to be a singleton in order to realize correct registering and unregistering of
- * the BroadcastReceiver, which listens on changes of network connectivity.
- *
- * @param dbHelper NoteSQLiteOpenHelper
- * @return NoteServerSyncHelper
- */
- public static synchronized NoteServerSyncHelper getInstance(NoteSQLiteOpenHelper dbHelper) {
- if (instance == null) {
- instance = new NoteServerSyncHelper(dbHelper);
- }
- return instance;
- }
-
- private final NoteSQLiteOpenHelper dbHelper;
- private final Context appContext;
-
- private CustomCertManager customCertManager;
- private ICustomCertService iCustomCertService;
-
- // Track network connection changes using a BroadcastReceiver
- private boolean networkConnected = false;
- private String syncOnlyOnWifiKey;
- private boolean syncOnlyOnWifi;
-
- /**
- * @see Do not make this a local variable.
- */
- private SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = (SharedPreferences prefs, String key) -> {
- if (syncOnlyOnWifiKey.equals(key)) {
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
- updateNetworkStatus();
- }
- };
-
- private final BroadcastReceiver networkReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateNetworkStatus();
- if (isSyncPossible()) {
- scheduleSync(false);
- }
- }
- };
-
- private boolean cert4androidReady = false;
- private final ServiceConnection certService = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
- iCustomCertService = ICustomCertService.Stub.asInterface(iBinder);
- cert4androidReady = true;
- if (isSyncPossible()) {
- scheduleSync(false);
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName componentName) {
- cert4androidReady = false;
- iCustomCertService = null;
- }
- };
-
- // current state of the synchronization
- private boolean syncActive = false;
- private boolean syncScheduled = false;
-
- // list of callbacks for both parts of synchronziation
- private List callbacksPush = new ArrayList<>();
- private List callbacksPull = new ArrayList<>();
-
-
- private NoteServerSyncHelper(NoteSQLiteOpenHelper db) {
- this.dbHelper = db;
- this.appContext = db.getContext().getApplicationContext();
- this.syncOnlyOnWifiKey = appContext.getResources().getString(R.string.pref_key_wifi_only);
- new Thread() {
- @Override
- public void run() {
- customCertManager = SupportUtil.getCertManager(appContext);
- }
- }.start();
-
- // Registers BroadcastReceiver to track network connection changes.
- appContext.registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.appContext);
- prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
-
- updateNetworkStatus();
- // bind to certifciate service to block sync attempts if service is not ready
- appContext.bindService(new Intent(appContext, CustomCertService.class), certService, Context.BIND_AUTO_CREATE);
- }
-
- @Override
- protected void finalize() throws Throwable {
- appContext.unregisterReceiver(networkReceiver);
- appContext.unbindService(certService);
- if (customCertManager != null) {
- customCertManager.close();
- }
- super.finalize();
- }
-
- public static boolean isConfigured(Context context) {
- return !PreferenceManager.getDefaultSharedPreferences(context).getString(SettingsActivity.SETTINGS_URL, SettingsActivity.DEFAULT_SETTINGS).isEmpty();
- }
-
- private android.accounts.Account[] getEeloAccountsOnDevice(AccountManager accountManager) {
- return accountManager.getAccountsByType(
- eelo_account_type);
- }
-
- public boolean isSyncEnabled() {
- AccountManager accountManager = AccountManager.get(appContext);
- boolean isEeloAccount = false;
-
- try {
- android.accounts.Account[] eeloAccounts = getEeloAccountsOnDevice(accountManager);
-
- for (android.accounts.Account eeloAccount : eeloAccounts) {
- String emailId = accountManager.getUserData(eeloAccount,
- account_email_address_key);
- if (PreferenceManager.getDefaultSharedPreferences(appContext).getString(
- SettingsActivity.SETTINGS_USERNAME, SettingsActivity.DEFAULT_SETTINGS)
- .equals(emailId)) {
- isEeloAccount = true;
- if (ContentResolver.getSyncAutomatically(eeloAccount,
- notes_content_authority)) {
- return true;
- }
- }
- }
- }
- catch (SecurityException e) {
- e.printStackTrace();
- }
-
- if (isEeloAccount) {
- return false;
- }
- return true;
- }
-
- /**
- * Synchronization is only possible, if there is an active network connection and
- * Cert4Android service is available.
- * NoteServerSyncHelper observes changes in the network connection.
- * The current state can be retrieved with this method.
- *
- * @return true if sync is possible, otherwise false.
- */
- public boolean isSyncPossible() {
- return networkConnected && isConfigured(appContext) && cert4androidReady;
- }
-
- public CustomCertManager getCustomCertManager() {
- return customCertManager;
- }
-
- public void checkCertificate(byte[] cert, boolean foreground, IOnCertificateDecision callback) throws RemoteException {
- iCustomCertService.checkTrusted(cert, true, foreground, callback);
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ICallback, contains one method that shall be executed.
- */
- public void addCallbackPush(ICallback callback) {
- callbacksPush.add(callback);
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ICallback, contains one method that shall be executed.
- */
- public void addCallbackPull(ICallback callback) {
- callbacksPull.add(callback);
- }
-
-
- /**
- * Schedules a synchronization and start it directly, if the network is connected and no
- * synchronization is currently running.
- *
- * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
- */
- public void scheduleSync(boolean onlyLocalChanges) {
- Log.d(getClass().getSimpleName(), "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (syncActive ? "sync active" : "sync NOT active") + ") ...");
- Log.d(getClass().getSimpleName(), "(network:" + networkConnected + "; conf:" + isConfigured(appContext) + "; cert4android:" + cert4androidReady + ")");
- if (isSyncEnabled()) {
- if (isSyncPossible() && (!syncActive || onlyLocalChanges)) {
- Log.d(getClass().getSimpleName(), "... starting now");
- SyncTask syncTask = new SyncTask(onlyLocalChanges);
- syncTask.addCallbacks(callbacksPush);
- callbacksPush = new ArrayList<>();
- if (!onlyLocalChanges) {
- syncTask.addCallbacks(callbacksPull);
- callbacksPull = new ArrayList<>();
- }
- syncTask.execute();
- } else if (!onlyLocalChanges) {
- Log.d(getClass().getSimpleName(), "... scheduled");
- syncScheduled = true;
- for (ICallback callback : callbacksPush) {
- callback.onScheduled();
- }
- } else {
- Log.d(getClass().getSimpleName(), "... do nothing");
- for (ICallback callback : callbacksPush) {
- callback.onScheduled();
- }
- }
- }
- }
-
- private void updateNetworkStatus() {
- ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
-
- if (activeInfo != null && activeInfo.isConnected()) {
- networkConnected =
- !syncOnlyOnWifi || ((ConnectivityManager) appContext
- .getSystemService(Context.CONNECTIVITY_SERVICE))
- .getNetworkInfo(ConnectivityManager.TYPE_WIFI).isConnected();
-
- if (networkConnected) {
- Log.d(NoteServerSyncHelper.class.getSimpleName(), "Network connection established.");
- } else {
- Log.d(NoteServerSyncHelper.class.getSimpleName(), "Network connected, but not used because only synced on wifi.");
- }
- } else {
- networkConnected = false;
- Log.d(NoteServerSyncHelper.class.getSimpleName(), "No network connection.");
- }
- }
-
- /**
- * SyncTask is an AsyncTask which performs the synchronization in a background thread.
- * Synchronization consists of two parts: pushLocalChanges and pullRemoteChanges.
- */
- private class SyncTask extends AsyncTask {
- private final boolean onlyLocalChanges;
- private final List callbacks = new ArrayList<>();
- private NotesClient client;
- private List exceptions = new ArrayList<>();
-
- public SyncTask(boolean onlyLocalChanges) {
- this.onlyLocalChanges = onlyLocalChanges;
- }
-
- public void addCallbacks(List callbacks) {
- this.callbacks.addAll(callbacks);
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- if (!onlyLocalChanges && syncScheduled) {
- syncScheduled = false;
- }
- syncActive = true;
- }
-
- @Override
- protected NotesClientUtil.LoginStatus doInBackground(Void... voids) {
- client = createNotesClient(); // recreate NoteClients on every sync in case the connection settings was changed
- Log.i(getClass().getSimpleName(), "STARTING SYNCHRONIZATION");
- //dbHelper.debugPrintFullDB();
- NotesClientUtil.LoginStatus status = NotesClientUtil.LoginStatus.OK;
- pushLocalChanges();
- if (!onlyLocalChanges) {
- status = pullRemoteChanges();
- }
- //dbHelper.debugPrintFullDB();
- Log.i(getClass().getSimpleName(), "SYNCHRONIZATION FINISHED");
- return status;
- }
-
- /**
- * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server.
- */
- private void pushLocalChanges() {
- Log.d(getClass().getSimpleName(), "pushLocalChanges()");
- List notes = dbHelper.getLocalModifiedNotes();
- for (DBNote note : notes) {
- Log.d(getClass().getSimpleName(), " Process Local Note: " + note);
- try {
- CloudNote remoteNote = null;
- switch (note.getStatus()) {
- case LOCAL_EDITED:
- Log.v(getClass().getSimpleName(), " ...create/edit");
- // if note is not new, try to edit it.
- if (note.getRemoteId() > 0) {
- Log.v(getClass().getSimpleName(), " ...try to edit");
- try {
- remoteNote = client.editNote(customCertManager, note).getNote();
- } catch (FileNotFoundException e) {
- // Note does not exists anymore
- }
- }
- // However, the note may be deleted on the server meanwhile; or was never synchronized -> (re)create
- // Please note, thas dbHelper.updateNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- if (remoteNote == null) {
- Log.v(getClass().getSimpleName(), " ...Note does not exist on server -> (re)create");
- remoteNote = client.createNote(customCertManager, note).getNote();
- }
- dbHelper.updateNote(note.getId(), remoteNote, note);
- break;
- case LOCAL_DELETED:
- if (note.getRemoteId() > 0) {
- Log.v(getClass().getSimpleName(), " ...delete (from server and local)");
- try {
- client.deleteNote(customCertManager, note.getRemoteId());
- } catch (FileNotFoundException e) {
- Log.v(getClass().getSimpleName(), " ...Note does not exist on server (anymore?) -> delete locally");
- }
- } else {
- Log.v(getClass().getSimpleName(), " ...delete (only local, since it was not synchronized)");
- }
- // Please note, thas dbHelper.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- dbHelper.deleteNote(note.getId(), DBStatus.LOCAL_DELETED);
- break;
- default:
- throw new IllegalStateException("Unknown State of Note: " + note);
- }
- } catch (IOException | JSONException e) {
- Log.e(getClass().getSimpleName(), "Exception", e);
- exceptions.add(e);
- }
- }
- }
-
- /**
- * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes.
- */
- private NotesClientUtil.LoginStatus pullRemoteChanges() {
- Log.d(getClass().getSimpleName(), "pullRemoteChanges()");
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(appContext);
- String lastETag = preferences.getString(SettingsActivity.SETTINGS_KEY_ETAG, null);
- long lastModified = preferences.getLong(SettingsActivity.SETTINGS_KEY_LAST_MODIFIED, 0);
- NotesClientUtil.LoginStatus status;
- try {
- Map idMap = dbHelper.getIdMap();
- ServerResponse.NotesResponse response = client.getNotes(customCertManager, lastModified, lastETag);
- List remoteNotes = response.getNotes();
- Set remoteIDs = new HashSet<>();
- // pull remote changes: update or create each remote note
- for (CloudNote remoteNote : remoteNotes) {
- Log.v(getClass().getSimpleName(), " Process Remote Note: " + remoteNote);
- remoteIDs.add(remoteNote.getRemoteId());
- if (remoteNote.getModified() == null) {
- Log.v(getClass().getSimpleName(), " ... unchanged");
- } else if (idMap.containsKey(remoteNote.getRemoteId())) {
- Log.v(getClass().getSimpleName(), " ... found -> Update");
- dbHelper.updateNote(idMap.get(remoteNote.getRemoteId()), remoteNote, null);
- } else {
- Log.v(getClass().getSimpleName(), " ... create");
- dbHelper.addNote(remoteNote);
- }
- }
- Log.d(getClass().getSimpleName(), " Remove remotely deleted Notes (only those without local changes)");
- // remove remotely deleted notes (only those without local changes)
- for (Map.Entry entry : idMap.entrySet()) {
- if (!remoteIDs.contains(entry.getKey())) {
- Log.v(getClass().getSimpleName(), " ... remove " + entry.getValue());
- dbHelper.deleteNote(entry.getValue(), DBStatus.VOID);
- }
- }
- status = NotesClientUtil.LoginStatus.OK;
-
- // update ETag and Last-Modified in order to reduce size of next response
- SharedPreferences.Editor editor = preferences.edit();
- String etag = response.getETag();
- if (etag != null && !etag.isEmpty()) {
- editor.putString(SettingsActivity.SETTINGS_KEY_ETAG, etag);
- } else {
- editor.remove(SettingsActivity.SETTINGS_KEY_ETAG);
- }
- long modified = response.getLastModified();
- if (modified != 0) {
- editor.putLong(SettingsActivity.SETTINGS_KEY_LAST_MODIFIED, modified);
- } else {
- editor.remove(SettingsActivity.SETTINGS_KEY_LAST_MODIFIED);
- }
- editor.apply();
- } catch (ServerResponse.NotModifiedException e) {
- Log.d(getClass().getSimpleName(), "No changes, nothing to do.");
- status = NotesClientUtil.LoginStatus.OK;
- } catch (IOException e) {
- Log.e(getClass().getSimpleName(), "Exception", e);
- exceptions.add(e);
- status = NotesClientUtil.LoginStatus.CONNECTION_FAILED;
- } catch (JSONException e) {
- Log.e(getClass().getSimpleName(), "Exception", e);
- exceptions.add(e);
- status = NotesClientUtil.LoginStatus.JSON_FAILED;
- }
- return status;
- }
-
- @Override
- protected void onPostExecute(NotesClientUtil.LoginStatus status) {
- super.onPostExecute(status);
- if (status != NotesClientUtil.LoginStatus.OK) {
- Toast.makeText(appContext, appContext.getString(foundation.e.notes.R.string.error_sync, appContext.getString(status.str)), Toast.LENGTH_LONG).show();
- for (Throwable e : exceptions) {
- Toast.makeText(appContext, e.getClass().getName() + ": " + e.getMessage(), Toast.LENGTH_LONG).show();
- }
- }
- syncActive = false;
- // notify callbacks
- for (ICallback callback : callbacks) {
- callback.onFinish();
- }
- dbHelper.notifyNotesChanged();
- // start next sync if scheduled meanwhile
- if (syncScheduled) {
- scheduleSync(false);
- }
- }
- }
-
- private NotesClient createNotesClient() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(appContext.getApplicationContext());
- String url = preferences.getString(SettingsActivity.SETTINGS_URL, SettingsActivity.DEFAULT_SETTINGS);
- String username = preferences.getString(SettingsActivity.SETTINGS_USERNAME, SettingsActivity.DEFAULT_SETTINGS);
- String password = preferences.getString(SettingsActivity.SETTINGS_PASSWORD, SettingsActivity.DEFAULT_SETTINGS);
- return new NotesClient(url, username, password);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/DisplayUtils.java b/app/src/main/java/foundation/e/notes/util/DisplayUtils.java
deleted file mode 100644
index 7efc13e22b50aa51b44bc30a3469bbea7920b45a..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/DisplayUtils.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Nextcloud Notes application
- *
- * @author Mario Danic
- * Copyright (C) 2018 Mario Danic
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package foundation.e.notes.util;
-
-import android.graphics.Typeface;
-import android.text.Spannable;
-import android.text.TextUtils;
-import android.text.style.CharacterStyle;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import androidx.annotation.ColorInt;
-
-public class DisplayUtils {
-
- public static Spannable searchAndColor(String text, Spannable spannable, String searchText, @ColorInt int color) {
-
- Object spansToRemove[] = spannable.getSpans(0, text.length(), Object.class);
- for(Object span: spansToRemove){
- if(span instanceof CharacterStyle)
- spannable.removeSpan(span);
- }
-
- if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
- return spannable;
- }
-
- Matcher m = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
- .matcher(text);
-
-
- while (m.find()) {
- int start = m.start();
- int end = m.end();
- spannable.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- spannable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
- return spannable;
- }
-
-}
diff --git a/app/src/main/java/foundation/e/notes/util/ExceptionHandler.java b/app/src/main/java/foundation/e/notes/util/ExceptionHandler.java
deleted file mode 100644
index 830dc7904387037deaca658dcd5de3b5b465db75..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/ExceptionHandler.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package foundation.e.notes.util;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import java.io.Serializable;
-
-import foundation.e.notes.android.activity.ExceptionActivity;
-
-import static foundation.e.notes.android.activity.ExceptionActivity.KEY_THROWABLE;
-
-public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
-
- private Activity context;
-
- public ExceptionHandler(Activity context) {
- super();
- this.context = context;
- }
-
- @Override
- public void uncaughtException(Thread t, Throwable e) {
- e.printStackTrace();
- Intent intent = new Intent(context.getApplicationContext(), ExceptionActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- Bundle extras = new Bundle();
- intent.putExtra(KEY_THROWABLE, (Serializable) e);
- extras.putSerializable(KEY_THROWABLE, e);
- intent.putExtras(extras);
- context.getApplicationContext().startActivity(intent);
- context.finish();
- Runtime.getRuntime().exit(0);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/ICallback.java b/app/src/main/java/foundation/e/notes/util/ICallback.java
deleted file mode 100644
index 8b2f332aa2dada703a61659517c491dca63afc33..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/ICallback.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package foundation.e.notes.util;
-
-/**
- * Callback
- * Created by stefan on 01.10.15.
- */
-public interface ICallback {
- void onFinish();
-
- void onScheduled();
-}
diff --git a/app/src/main/java/foundation/e/notes/util/MarkDownUtil.java b/app/src/main/java/foundation/e/notes/util/MarkDownUtil.java
deleted file mode 100644
index d603a2b7bb17622118b7e3f4d8eef0ad05a96b23..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/MarkDownUtil.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package foundation.e.notes.util;
-
-import android.content.Context;
-
-import androidx.core.content.ContextCompat;
-import androidx.core.content.res.ResourcesCompat;
-
-import com.yydcdut.rxmarkdown.RxMDConfiguration;
-import com.yydcdut.rxmarkdown.RxMDConfiguration.Builder;
-
-import foundation.e.notes.R;
-
-/**
- * Created by stefan on 07.12.16.
- */
-
-public class MarkDownUtil {
-
- /**
- * Ensures every instance of RxMD uses the same configuration
- *
- * @param context Context
- * @return RxMDConfiguration
- */
- public static Builder getMarkDownConfiguration(Context context) {
- return new RxMDConfiguration.Builder(context)
- .setUnOrderListColor(ContextCompat.getColor(context, R.color.color_default_secondary_text))
- .setCodeBgColor(ContextCompat.getColor(context, R.color.color_default_primary_text))
- .setHeader2RelativeSize(1.35f)
- .setHeader3RelativeSize(1.25f)
- .setHeader4RelativeSize(1.15f)
- .setHeader5RelativeSize(1.1f)
- .setHeader6RelativeSize(1.05f)
- .setHorizontalRulesHeight(2)
- .setLinkFontColor(ContextCompat.getColor(context, R.color.color_default_primary_text));
- }
-
- public static Builder getMarkDownConfiguration(Context context, Boolean darkTheme) {
- return new RxMDConfiguration.Builder(context)
- .setUnOrderListColor(ResourcesCompat.getColor(context.getResources(),
- darkTheme ? R.color.widget_fg_dark_theme : R.color.widget_fg_default, null))
- .setHeader2RelativeSize(1.35f)
- .setHeader3RelativeSize(1.25f)
- .setHeader4RelativeSize(1.15f)
- .setHeader5RelativeSize(1.1f)
- .setHeader6RelativeSize(1.05f)
- .setHorizontalRulesHeight(2)
- .setLinkFontColor(ResourcesCompat.getColor(context.getResources(), R.color.color_default_primary_text, null));
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/NoteUtil.java b/app/src/main/java/foundation/e/notes/util/NoteUtil.java
deleted file mode 100644
index a4537f0fd2846ecfd81114a57a7a08adaf90e627..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/NoteUtil.java
+++ /dev/null
@@ -1,136 +0,0 @@
-package foundation.e.notes.util;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.regex.Pattern;
-
-import foundation.e.notes.R;
-
-/**
- * Provides basic functionality for Note operations.
- * Created by stefan on 06.10.15.
- */
-public class NoteUtil {
- private static final Pattern pLists = Pattern.compile("^\\s*[*+-]\\s+", Pattern.MULTILINE);
- private static final Pattern pHeadings = Pattern.compile("^#+\\s+(.*?)\\s*#*$", Pattern.MULTILINE);
- private static final Pattern pHeadingLine = Pattern.compile("^(?:=*|-*)$", Pattern.MULTILINE);
- private static final Pattern pEmphasis = Pattern.compile("(\\*+|_+)(.*?)\\1", Pattern.MULTILINE);
- private static final Pattern pSpace1 = Pattern.compile("^\\s+", Pattern.MULTILINE);
- private static final Pattern pSpace2 = Pattern.compile("\\s+$", Pattern.MULTILINE);
-
-
- /**
- * Strips all MarkDown from the given String
- *
- * @param s String - MarkDown
- * @return Plain Text-String
- */
- @NonNull
- public static String removeMarkDown(@Nullable String s) {
- if (s == null)
- return "";
- s = pLists.matcher(s).replaceAll("");
- s = pHeadings.matcher(s).replaceAll("$1");
- s = pHeadingLine.matcher(s).replaceAll("");
- s = pEmphasis.matcher(s).replaceAll("$2");
- s = pSpace1.matcher(s).replaceAll("");
- s = pSpace2.matcher(s).replaceAll("");
- return s;
- }
-
- /**
- * Checks if a line is empty.
- *
- * " " -> empty
- * "\n" -> empty
- * "\n " -> empty
- * " \n" -> empty
- * " \n " -> empty
- *
- *
- * @param line String - a single Line which ends with \n
- * @return boolean isEmpty
- */
- private static boolean isEmptyLine(@Nullable String line) {
- return removeMarkDown(line).trim().length() == 0;
- }
-
- /**
- * Truncates a string to a desired maximum length.
- * Like String.substring(int,int), but throw no exception if desired length is longer than the string.
- *
- * @param str String to truncate
- * @param len Maximum length of the resulting string
- * @return truncated string
- */
- @NonNull
- private static String truncateString(@NonNull String str, int len) {
- return str.substring(0, Math.min(len, str.length()));
- }
-
- /**
- * Generates an excerpt of a content String (reads second line which is not empty)
- *
- * @param content String
- * @return excerpt String
- */
- @NonNull
- public static String generateNoteExcerpt(@NonNull String content) {
- if (content.contains("\n"))
- return truncateString(removeMarkDown(content.replaceFirst("^.*\n", "")), 200).replace("\n", " ");
- else
- return "";
- }
-
- @NonNull
- public static String generateNonEmptyNoteTitle(@NonNull String content, Context context) {
- String title = generateNoteTitle(content);
- if (title.isEmpty()) {
- title = context.getString(R.string.action_create);
- }
- return title;
- }
-
- /**
- * Generates a title of a content String (reads fist linew which is not empty)
- *
- * @param content String
- * @return excerpt String
- */
- @NonNull
- static String generateNoteTitle(@NonNull String content) {
- return getLineWithoutMarkDown(content, 0);
- }
-
- /**
- * Reads the requested line and strips all MarkDown. If line is empty, it will go ahead to find the next not-empty line.
- *
- * @param content String
- * @param lineNumber int
- * @return lineContent String
- */
- @NonNull
- private static String getLineWithoutMarkDown(@NonNull String content, int lineNumber) {
- String line = "";
- if (content.contains("\n")) {
- String[] lines = content.split("\n");
- int currentLine = lineNumber;
- while (currentLine < lines.length && NoteUtil.isEmptyLine(lines[currentLine])) {
- currentLine++;
- }
- if (currentLine < lines.length) {
- line = NoteUtil.removeMarkDown(lines[currentLine]);
- }
- } else {
- line = content;
- }
- return line;
- }
-
- @NonNull
- public static String extendCategory(@NonNull String category) {
- return category.replace("/", " / ");
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/Notes.java b/app/src/main/java/foundation/e/notes/util/Notes.java
deleted file mode 100644
index d96c39a2dfce0c1b8d060c26ca90efa8d5835aff..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/Notes.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package foundation.e.notes.util;
-
-import android.app.Application;
-import android.content.Context;
-import android.content.res.Configuration;
-
-public class Notes extends Application {
-
- public static boolean getAppTheme(Context context) {
- int nightModeFlags = context.getApplicationContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
- return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/NotesClient.java b/app/src/main/java/foundation/e/notes/util/NotesClient.java
deleted file mode 100644
index a588c842883843a701ea3d7c5ca83f751b59688e..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/NotesClient.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package foundation.e.notes.util;
-
-import androidx.annotation.WorkerThread;
-import android.util.Base64;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.BuildConfig;
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.util.ServerResponse.NoteResponse;
-import foundation.e.notes.util.ServerResponse.NotesResponse;
-
-@WorkerThread
-public class NotesClient {
-
- /**
- * This entity class is used to return relevant data of the HTTP reponse.
- */
- public static class ResponseData {
- private final String content;
- private final String etag;
- private final long lastModified;
-
- public ResponseData(String content, String etag, long lastModified) {
- this.content = content;
- this.etag = etag;
- this.lastModified = lastModified;
- }
-
- public String getContent() {
- return content;
- }
-
- public String getETag() {
- return etag;
- }
-
- public long getLastModified() {
- return lastModified;
- }
- }
-
- public static final String METHOD_GET = "GET";
- public static final String METHOD_PUT = "PUT";
- public static final String METHOD_POST = "POST";
- public static final String METHOD_DELETE = "DELETE";
- public static final String JSON_ID = "id";
- public static final String JSON_TITLE = "title";
- public static final String JSON_CONTENT = "content";
- public static final String JSON_FAVORITE = "favorite";
- public static final String JSON_CATEGORY = "category";
- public static final String JSON_ETAG = "etag";
- public static final String JSON_MODIFIED = "modified";
- private static final String application_json = "application/json";
- private String url = "";
- private String username = "";
- private String password = "";
-
- public NotesClient(String url, String username, String password) {
- this.url = url;
- this.username = username;
- this.password = password;
- }
-
- public NotesResponse getNotes(CustomCertManager ccm, long lastModified, String lastETag) throws JSONException, IOException {
- String url = "notes";
- if (lastModified > 0) {
- url += "?pruneBefore=" + lastModified;
- }
- return new NotesResponse(requestServer(ccm, url, METHOD_GET, null, lastETag));
- }
-
- /**
- * Fetches a Note by ID from Server
- *
- * @param id long - ID of the wanted note
- * @return Requested Note
- * @throws JSONException
- * @throws IOException
- */
- @SuppressWarnings("unused")
- public NoteResponse getNoteById(CustomCertManager ccm, long id) throws JSONException, IOException {
- return new NoteResponse(requestServer(ccm, "notes/" + id, METHOD_GET, null, null));
- }
-
- private NoteResponse putNote(CustomCertManager ccm, CloudNote note, String path, String method) throws JSONException, IOException {
- JSONObject paramObject = new JSONObject();
- paramObject.accumulate(JSON_CONTENT, note.getContent());
- paramObject.accumulate(JSON_MODIFIED, note.getModified().getTimeInMillis() / 1000);
- paramObject.accumulate(JSON_FAVORITE, note.isFavorite());
- paramObject.accumulate(JSON_CATEGORY, note.getCategory());
- return new NoteResponse(requestServer(ccm, path, method, paramObject, null));
- }
-
-
- /**
- * Creates a Note on the Server
- *
- * @param note {@link CloudNote} - the new Note
- * @return Created Note including generated Title, ID and lastModified-Date
- * @throws JSONException
- * @throws IOException
- */
- public NoteResponse createNote(CustomCertManager ccm, CloudNote note) throws JSONException, IOException {
- return putNote(ccm, note, "notes", METHOD_POST);
- }
-
- public NoteResponse editNote(CustomCertManager ccm, CloudNote note) throws JSONException, IOException {
- return putNote(ccm, note, "notes/" + note.getRemoteId(), METHOD_PUT);
- }
-
- public void deleteNote(CustomCertManager ccm, long noteId) throws IOException {
- this.requestServer(ccm, "notes/" + noteId, METHOD_DELETE, null, null);
- }
-
- /**
- * Request-Method for POST, PUT with or without JSON-Object-Parameter
- *
- * @param target Filepath to the wanted function
- * @param method GET, POST, DELETE or PUT
- * @param params JSON Object which shall be transferred to the server.
- * @return Body of answer
- * @throws MalformedURLException
- * @throws IOException
- */
- private ResponseData requestServer(CustomCertManager ccm, String target, String method, JSONObject params, String lastETag)
- throws IOException {
- StringBuffer result = new StringBuffer();
- // setup connection
- String targetURL = url + "index.php/apps/notes/api/v0.2/" + target;
- HttpURLConnection con = SupportUtil.getHttpURLConnection(ccm, targetURL);
- con.setRequestMethod(method);
- con.setRequestProperty(
- "Authorization",
- "Basic " + Base64.encodeToString((username + ":" + password).getBytes(), Base64.NO_WRAP));
- // https://github.com/square/retrofit/issues/805#issuecomment-93426183
- con.setRequestProperty( "Connection", "Close");
- con.setRequestProperty("User-Agent", "nextcloud-notes/" + BuildConfig.VERSION_NAME + " (Android)");
- if (lastETag != null && METHOD_GET.equals(method)) {
- con.setRequestProperty("If-None-Match", lastETag);
- }
- con.setConnectTimeout(10 * 1000); // 10 seconds
- Log.d(getClass().getSimpleName(), method + " " + targetURL);
- // send request data (optional)
- byte[] paramData = null;
- if (params != null) {
- paramData = params.toString().getBytes();
- Log.d(getClass().getSimpleName(), "Params: " + params);
- con.setFixedLengthStreamingMode(paramData.length);
- con.setRequestProperty("Content-Type", application_json);
- con.setDoOutput(true);
- OutputStream os = con.getOutputStream();
- os.write(paramData);
- os.flush();
- os.close();
- }
- // read response data
- int responseCode = con.getResponseCode();
- Log.d(getClass().getSimpleName(), "HTTP response code: " + responseCode);
-
- if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
- throw new ServerResponse.NotModifiedException();
- }
-
- BufferedReader rd = new BufferedReader(new InputStreamReader(con.getInputStream()));
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- // create response object
- String etag = con.getHeaderField("ETag");
- long lastModified = con.getHeaderFieldDate("Last-Modified", 0) / 1000;
- Log.i(getClass().getSimpleName(), "Result length: " + result.length() + (paramData == null ? "" : "; Request length: " + paramData.length));
- Log.d(getClass().getSimpleName(), "ETag: " + etag + "; Last-Modified: " + lastModified + " (" + con.getHeaderField("Last-Modified") + ")");
- // return these header fields since they should only be saved after successful processing the result!
- return new ResponseData(result.toString(), etag, lastModified);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/NotesClientUtil.java b/app/src/main/java/foundation/e/notes/util/NotesClientUtil.java
deleted file mode 100644
index e57ed810406ee6c1a0880473e531be7c94a85daf..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/NotesClientUtil.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package foundation.e.notes.util;
-
-import androidx.annotation.StringRes;
-import android.util.Base64;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.SocketTimeoutException;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.notes.R;
-
-/**
- * Utils for Validation etc
- * Created by stefan on 25.09.15.
- */
-public class NotesClientUtil {
-
- public enum LoginStatus {
- OK(0),
- AUTH_FAILED(R.string.error_username_password_invalid),
- CONNECTION_FAILED(R.string.error_io),
- NO_NETWORK(R.string.error_no_network),
- JSON_FAILED(R.string.error_json),
- SERVER_FAILED(R.string.error_server);
-
- @StringRes
- public final int str;
-
- LoginStatus(@StringRes int str) {
- this.str = str;
- }
- }
-
- /**
- * Checks if the given url String starts with http:// or https://
- *
- * @param url String
- * @return true, if the given String is only http
- */
- public static boolean isHttp(String url) {
- return url != null && url.length() > 4 && url.startsWith("http") && url.charAt(4) != 's';
- }
-
- /**
- * Strips the api part from the path of a given url, handles trailing slash and missing protocol
- *
- * @param url String
- * @return formatted URL
- */
- public static String formatURL(String url) {
- if (!url.endsWith("/")) {
- url += "/";
- }
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
- url = "https://" + url;
- }
- String[] replacements = new String[]{"notes/", "v0.2/", "api/", "notes/", "apps/", "index.php/"};
- for (String replacement : replacements) {
- if (url.endsWith(replacement)) {
- url = url.substring(0, url.length() - replacement.length());
- }
- }
- return url;
- }
-
- /**
- * @param url String
- * @param username String
- * @param password String
- * @return Username and Password are a valid Login-Combination for the given URL.
- */
- public static LoginStatus isValidLogin(CustomCertManager ccm, String url, String username, String password) {
- try {
- String targetURL = url + "index.php/apps/notes/api/v0.2/notes";
- HttpURLConnection con = SupportUtil.getHttpURLConnection(ccm, targetURL);
- con.setRequestMethod("GET");
- con.setRequestProperty(
- "Authorization",
- "Basic "
- + new String(Base64.encode((username + ":"
- + password).getBytes(), Base64.NO_WRAP)));
- con.setConnectTimeout(10 * 1000); // 10 seconds
- con.connect();
-
- Log.v(NotesClientUtil.class.getSimpleName(), "Establishing connection to server");
- if (con.getResponseCode() == 200) {
- Log.v(NotesClientUtil.class.getSimpleName(), "" + con.getResponseMessage());
- StringBuilder result = new StringBuilder();
- BufferedReader rd = new BufferedReader(new InputStreamReader(con.getInputStream()));
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- Log.v(NotesClientUtil.class.getSimpleName(), result.toString());
- new JSONArray(result.toString());
- return LoginStatus.OK;
- } else if (con.getResponseCode() >= 401 && con.getResponseCode() <= 403) {
- return LoginStatus.AUTH_FAILED;
- } else {
- return LoginStatus.SERVER_FAILED;
- }
- } catch (MalformedURLException | SocketTimeoutException e) {
- Log.e(NotesClientUtil.class.getSimpleName(), "Exception", e);
- return LoginStatus.CONNECTION_FAILED;
- } catch (IOException e) {
- Log.e(NotesClientUtil.class.getSimpleName(), "Exception", e);
- return LoginStatus.CONNECTION_FAILED;
- } catch (JSONException e) {
- Log.e(NotesClientUtil.class.getSimpleName(), "Exception", e);
- return LoginStatus.JSON_FAILED;
- }
- }
-
- /**
- * Pings a server and checks if there is a installed ownCloud instance
- *
- * @param url String URL to server
- * @return true if there is a installed instance, false if not
- */
- public static boolean isValidURL(CustomCertManager ccm, String url) {
- StringBuilder result = new StringBuilder();
- try {
- HttpURLConnection con = SupportUtil.getHttpURLConnection(ccm, url + "status.php");
- con.setRequestMethod(NotesClient.METHOD_GET);
- con.setConnectTimeout(10 * 1000); // 10 seconds
- BufferedReader rd = new BufferedReader(new InputStreamReader(con.getInputStream()));
- String line;
- while ((line = rd.readLine()) != null) {
- result.append(line);
- }
- JSONObject response = new JSONObject(result.toString());
- return response.getBoolean("installed");
- } catch (IOException | JSONException | NullPointerException e) {
- return false;
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/notes/util/ServerResponse.java b/app/src/main/java/foundation/e/notes/util/ServerResponse.java
deleted file mode 100644
index 0c2644a78621960ae9189421046b43b81b29370a..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/ServerResponse.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package foundation.e.notes.util;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.List;
-
-import foundation.e.notes.model.CloudNote;
-import foundation.e.notes.model.CloudNote;
-
-/**
- * Provides entity classes for handling server responses with a single note ({@link NoteResponse}) or a list of notes ({@link NotesResponse}).
- */
-public class ServerResponse {
-
- public static class NotModifiedException extends IOException {
- }
-
- public static class NoteResponse extends ServerResponse {
- public NoteResponse(NotesClient.ResponseData response) {
- super(response);
- }
-
- public CloudNote getNote() throws JSONException {
- return getNoteFromJSON(new JSONObject(getContent()));
- }
- }
-
- public static class NotesResponse extends ServerResponse {
- public NotesResponse(NotesClient.ResponseData response) {
- super(response);
- }
-
- public List getNotes() throws JSONException {
- List notesList = new ArrayList<>();
- JSONArray notes = new JSONArray(getContent());
- for (int i = 0; i < notes.length(); i++) {
- JSONObject json = notes.getJSONObject(i);
- notesList.add(getNoteFromJSON(json));
- }
- return notesList;
- }
- }
-
-
- private final NotesClient.ResponseData response;
-
- public ServerResponse(NotesClient.ResponseData response) {
- this.response = response;
- }
-
- protected String getContent() {
- return response.getContent();
- }
-
- public String getETag() {
- return response.getETag();
- }
-
- public long getLastModified() {
- return response.getLastModified();
- }
-
- protected CloudNote getNoteFromJSON(JSONObject json) throws JSONException {
- long id = 0;
- String title = "";
- String content = "";
- Calendar modified = null;
- boolean favorite = false;
- String category = null;
- String etag = null;
- if (!json.isNull(NotesClient.JSON_ID)) {
- id = json.getLong(NotesClient.JSON_ID);
- }
- if (!json.isNull(NotesClient.JSON_TITLE)) {
- title = json.getString(NotesClient.JSON_TITLE);
- }
- if (!json.isNull(NotesClient.JSON_CONTENT)) {
- content = json.getString(NotesClient.JSON_CONTENT);
- }
- if (!json.isNull(NotesClient.JSON_MODIFIED)) {
- modified = GregorianCalendar.getInstance();
- modified.setTimeInMillis(json.getLong(NotesClient.JSON_MODIFIED) * 1000);
- }
- if (!json.isNull(NotesClient.JSON_FAVORITE)) {
- favorite = json.getBoolean(NotesClient.JSON_FAVORITE);
- }
- if (!json.isNull(NotesClient.JSON_CATEGORY)) {
- category = json.getString(NotesClient.JSON_CATEGORY);
- }
- if (!json.isNull(NotesClient.JSON_ETAG)) {
- etag = json.getString(NotesClient.JSON_ETAG);
- }
- return new CloudNote(id, modified, title, content, favorite, category, etag);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/StyleCallback.java b/app/src/main/java/foundation/e/notes/util/StyleCallback.java
deleted file mode 100644
index f3d5d06d64d2575fe8f518fe0ef91c13979f5518..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/StyleCallback.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package foundation.e.notes.util;
-
-import android.graphics.Typeface;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.style.StyleSpan;
-import android.util.SparseIntArray;
-import android.view.ActionMode;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.widget.EditText;
-
-import foundation.e.notes.R;
-
-public class StyleCallback implements ActionMode.Callback {
-
- private EditText editText;
-
- public StyleCallback(EditText editText) {
- this.editText = editText;
- }
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- MenuInflater inflater = mode.getMenuInflater();
- inflater.inflate(R.menu.style, menu);
- menu.removeItem(android.R.id.selectAll);
-
- SparseIntArray styleFormatMap = new SparseIntArray();
- styleFormatMap.append(R.id.bold, Typeface.BOLD);
- styleFormatMap.append(R.id.italic, Typeface.ITALIC);
-
- MenuItem item;
- CharSequence title;
- SpannableStringBuilder ssb;
-
- for (int i = 0; i < styleFormatMap.size(); i++) {
- item = menu.findItem(styleFormatMap.keyAt(i));
- title = item.getTitle();
- ssb = new SpannableStringBuilder(title);
- ssb.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0);
- item.setTitle(ssb);
- }
-
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- return false;
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- int start = editText.getSelectionStart();
- int end = editText.getSelectionEnd();
- SpannableStringBuilder ssb = new SpannableStringBuilder(editText.getText());
- final String markdown;
-
-
- switch (item.getItemId()) {
- case R.id.bold:
- markdown = "**";
- if (hasAlreadyMarkdown(start, end, markdown)) {
- this.removeMarkdown(ssb, start, end, markdown);
- } else {
- this.addMarkdown(ssb, start, end, markdown, Typeface.BOLD);
- }
- editText.setText(ssb);
- editText.setSelection(end + markdown.length() * 2);
- break;
- case R.id.italic:
- markdown = "*";
- if (hasAlreadyMarkdown(start, end, markdown)) {
- this.removeMarkdown(ssb, start, end, markdown);
- } else {
- this.addMarkdown(ssb, start, end, markdown, Typeface.ITALIC);
- }
- editText.setText(ssb);
- editText.setSelection(end + markdown.length() * 2);
- break;
- case R.id.link:
- boolean textToFormatIsLink = TextUtils.indexOf(editText.getText().subSequence(start, end), "http") == 0;
- if(textToFormatIsLink) {
- ssb.insert(end, ")");
- ssb.insert(start, "[](");
- } else {
- ssb.insert(end, "]()");
- ssb.insert(start, "[");
- }
- end++;
- ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
- editText.setText(ssb);
- if(textToFormatIsLink) {
- editText.setSelection(start + 1);
- } else {
- editText.setSelection(end + 2); // after ](
- }
- return true;
- }
- return false;
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
-
- }
-
- private boolean hasAlreadyMarkdown(int start, int end, String markdown) {
- return start > markdown.length() && markdown.contentEquals(editText.getText().subSequence(start - markdown.length(), start)) &&
- editText.getText().length() > end + markdown.length() && markdown.contentEquals(editText.getText().subSequence(end, end + markdown.length()));
- }
-
- private void removeMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown) {
- // FIXME disabled, because it does not work properly and might cause data loss
- // ssb.delete(start - markdown.length(), start);
- // ssb.delete(end - markdown.length(), end);
- // ssb.setSpan(new StyleSpan(Typeface.NORMAL), start, end, 1);
- }
-
- private void addMarkdown(SpannableStringBuilder ssb, int start, int end, String markdown, int typeface) {
- ssb.insert(end, markdown);
- ssb.insert(start, markdown);
- editText.getText().charAt(start);
- editText.getText().charAt(start + 1);
- end += markdown.length() * 2;
- ssb.setSpan(new StyleSpan(typeface), start, end, 1);
- }
-}
diff --git a/app/src/main/java/foundation/e/notes/util/SupportUtil.java b/app/src/main/java/foundation/e/notes/util/SupportUtil.java
deleted file mode 100644
index b8a77976d437ad6efcb5d5e989bcbadcf0a7d0ee..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/notes/util/SupportUtil.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package foundation.e.notes.util;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.preference.PreferenceManager;
-import android.text.Html;
-import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
-import android.widget.TextView;
-
-import androidx.annotation.WorkerThread;
-
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-
-import foundation.e.cert4android.CustomCertManager;
-import foundation.e.notes.R;
-
-/**
- * Some helper functionality in alike the Android support library.
- * Currently, it offers methods for working with HTML string resources.
- */
-public class SupportUtil {
-
- /**
- * Creates a {@link Spanned} from a HTML string on all SDK versions.
- *
- * @param source Source string with HTML markup
- * @return Spannable for using in a {@link TextView}
- * @see Html#fromHtml(String)
- * @see Html#fromHtml(String, int)
- */
- public static Spanned fromHtml(String source) {
- if (Build.VERSION.SDK_INT >= 24) {
- return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY);
- } else {
- return Html.fromHtml(source);
- }
- }
-
- /**
- * Fills a {@link TextView} with HTML content and activates links in that {@link TextView}.
- *
- * @param view The {@link TextView} which should be filled.
- * @param stringId The string resource containing HTML tags (escaped by <
)
- * @param formatArgs Arguments for the string resource.
- */
- public static void setHtml(TextView view, int stringId, Object... formatArgs) {
- view.setText(SupportUtil.fromHtml(view.getResources().getString(stringId, formatArgs)));
- view.setMovementMethod(LinkMovementMethod.getInstance());
- }
-
- /**
- * Create a new {@link HttpURLConnection} for strUrl.
- * If protocol equals https, then install CustomCertManager in {@link SSLContext}.
- *
- * @param ccm
- * @param strUrl
- * @return HttpURLConnection with custom trust manager
- * @throws MalformedURLException
- * @throws IOException
- */
- public static HttpURLConnection getHttpURLConnection(CustomCertManager ccm, String strUrl) throws MalformedURLException, IOException {
- URL url = new URL(strUrl);
- HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
- if (ccm != null && url.getProtocol().equals("https")) {
- HttpsURLConnection httpsCon = (HttpsURLConnection) httpCon;
- httpsCon.setHostnameVerifier(ccm.hostnameVerifier(httpsCon.getHostnameVerifier()));
- try {
- SSLContext sslContext = SSLContext.getInstance("TLS");
- sslContext.init(null, new TrustManager[]{ccm}, null);
- httpsCon.setSSLSocketFactory(sslContext.getSocketFactory());
- } catch (NoSuchAlgorithmException e) {
- Log.e(SupportUtil.class.getSimpleName(), "Exception", e);
- // ignore, use default TrustManager
- } catch (KeyManagementException e) {
- Log.e(SupportUtil.class.getSimpleName(), "Exception", e);
- // ignore, use default TrustManager
- }
- }
- return httpCon;
- }
-
- @WorkerThread
- public static CustomCertManager getCertManager(Context ctx) {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ctx);
- return new CustomCertManager(ctx, preferences.getBoolean(ctx.getString(R.string.pref_key_trust_system_certs), true), true, true);
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f0fb28030e5a179d8a502b53a6f3e6f7cd4b47d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/AppendToNoteActivity.java
@@ -0,0 +1,61 @@
+package it.niedermann.owncloud.notes;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.owncloud.notes.main.MainActivity;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.util.ShareUtil;
+
+public class AppendToNoteActivity extends MainActivity {
+
+ private static final String TAG = AppendToNoteActivity.class.getSimpleName();
+
+ String receivedText = "";
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ receivedText = ShareUtil.extractSharedText(getIntent());
+ @Nullable final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setTitle(R.string.append_to_note);
+ } else {
+ Log.e(TAG, "SupportActionBar is null. Expected toolbar to be present to set a title.");
+ }
+ binding.activityNotesListView.searchToolbar.setSubtitle(receivedText);
+ }
+
+ @Override
+ public void onNoteClick(int position, View v) {
+ if (!TextUtils.isEmpty(receivedText)) {
+ final var fullNote$ = mainViewModel.getFullNote$(((Note) adapter.getItem(position)).getId());
+ fullNote$.observe(this, (fullNote) -> {
+ fullNote$.removeObservers(this);
+ final String oldContent = fullNote.getContent();
+ String newContent;
+ if (!TextUtils.isEmpty(oldContent)) {
+ newContent = oldContent + "\n\n" + receivedText;
+ } else {
+ newContent = receivedText;
+ }
+ final var updateLiveData = mainViewModel.updateNoteAndSync(fullNote, newContent, null);
+ updateLiveData.observe(this, (next) -> {
+ Toast.makeText(this, getString(R.string.added_content, receivedText), Toast.LENGTH_SHORT).show();
+ updateLiveData.removeObservers(this);
+ });
+ });
+ } else {
+ Toast.makeText(this, R.string.shared_text_empty, Toast.LENGTH_SHORT).show();
+ }
+ finish();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..91ef3724af42b6dbf808df91eee8ee131c1a6adc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/FormattingHelpActivity.java
@@ -0,0 +1,228 @@
+package it.niedermann.owncloud.notes;
+
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.util.TypedValue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedActivity;
+import it.niedermann.owncloud.notes.databinding.ActivityFormattingHelpBinding;
+
+import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
+
+public class FormattingHelpActivity extends BrandedActivity {
+
+ private ActivityFormattingHelpBinding binding;
+
+ private static final String lineBreak = "\n";
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ binding = ActivityFormattingHelpBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setSupportActionBar(binding.toolbar);
+
+ binding.contentContextBasedFormatting.setMarkdownString(buildContextBasedFormattingHelp());
+ binding.content.setMovementMethod(LinkMovementMethod.getInstance());
+ binding.content.setMarkdownString(buildFormattingHelp());
+
+ final var sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ binding.content.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(this, sp));
+ if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
+ binding.content.setTypeface(Typeface.MONOSPACE);
+ }
+ }
+
+ @NonNull
+ private String buildContextBasedFormattingHelp() {
+ return getString(R.string.formatting_help_title, getString(R.string.formatting_help_cbf_title)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_cbf_body_1) + lineBreak +
+ getString(R.string.formatting_help_cbf_body_2,
+ getString(R.string.formatting_help_codefence_inline, getString(android.R.string.cut)),
+ getString(R.string.formatting_help_codefence_inline, getString(android.R.string.copy)),
+ getString(R.string.formatting_help_codefence_inline, getString(android.R.string.selectAll)),
+ getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_link)),
+ getString(R.string.formatting_help_codefence_inline, getString(R.string.simple_checkbox))
+ );
+ }
+
+ @NonNull
+ private String buildFormattingHelp() {
+ final String indention = " ";
+ final String divider = getString(R.string.formatting_help_divider);
+ final String codefence = getString(R.string.formatting_help_codefence);
+ final String outerCodefence = getString(R.string.formatting_help_codefence_outer);
+
+ int numberedListItem = 1;
+ final String lists = getString(R.string.formatting_help_lists_body_1) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_2)) + lineBreak +
+ getString(R.string.formatting_help_ol, numberedListItem++, getString(R.string.formatting_help_lists_body_3)) + lineBreak +
+ getString(R.string.formatting_help_ol, numberedListItem, getString(R.string.formatting_help_lists_body_4)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_lists_body_5) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_6)) + lineBreak +
+ getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_7)) + lineBreak +
+ indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_8)) + lineBreak +
+ indention + getString(R.string.formatting_help_ul, getString(R.string.formatting_help_lists_body_9)) + lineBreak;
+
+ final String checkboxes = getString(R.string.formatting_help_checkboxes_body_1) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_checkbox_checked, getString(R.string.formatting_help_checkboxes_body_2)) + lineBreak +
+ getString(R.string.formatting_help_checkbox_unchecked, getString(R.string.formatting_help_checkboxes_body_3)) + lineBreak;
+
+ final String structuredDocuments = getString(R.string.formatting_help_structured_documents_body_1, "`#`", "`##`") + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title_level_3, getString(R.string.formatting_help_structured_documents_body_2)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_structured_documents_body_3, "`#`", "`######`") + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_structured_documents_body_4, getString(R.string.formatting_help_quote_keyword)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_5)) + lineBreak +
+ getString(R.string.formatting_help_quote, getString(R.string.formatting_help_structured_documents_body_6)) + lineBreak;
+
+ final String javascript = getString(R.string.formatting_help_javascript_1) + lineBreak +
+ indention + indention + getString(R.string.formatting_help_javascript_2) + lineBreak +
+ getString(R.string.formatting_help_javascript_3) + lineBreak;
+
+ final int column_count = 3;
+ final int row_count = 3;
+ final StringBuilder table = new StringBuilder();
+ table.append("|");
+ for (int i = 1; i <= column_count; i++) {
+ table.append(" ").append(getString(R.string.formatting_help_tables_column, i)).append(" |");
+ }
+ table.append("\n");
+ table.append("|");
+ for (int i = 0; i < column_count; i++) {
+ table.append(" --- |");
+ }
+ table.append("\n");
+ for (int i = 1; i <= row_count; i++) {
+ table.append("|");
+ for (int j = 1; j <= column_count; j++) {
+ table.append(" ").append(getString(R.string.formatting_help_tables_value, i * j)).append(" |");
+ }
+ table.append("\n");
+ }
+
+ return divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_text_title)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_text_body,
+ getString(R.string.formatting_help_bold),
+ getString(R.string.formatting_help_italic),
+ getString(R.string.formatting_help_strike_through)
+ ) + lineBreak +
+ lineBreak +
+ codefence + lineBreak +
+ getString(R.string.formatting_help_text_body,
+ getString(R.string.formatting_help_bold),
+ getString(R.string.formatting_help_italic),
+ getString(R.string.formatting_help_strike_through)
+ ) + lineBreak +
+ codefence + lineBreak +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_lists_title)) + lineBreak +
+ lineBreak +
+ lists +
+ lineBreak +
+ codefence + lineBreak +
+ lists +
+ codefence + lineBreak +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_checkboxes_title)) + lineBreak +
+ lineBreak +
+ checkboxes +
+ lineBreak +
+ codefence + lineBreak +
+ checkboxes +
+ codefence + lineBreak +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_structured_documents_title)) + lineBreak +
+ lineBreak +
+ structuredDocuments +
+ lineBreak +
+ codefence + lineBreak +
+ structuredDocuments +
+ codefence + lineBreak +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_code_title)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_code_body_1) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_codefence_inline_escaped, getString(R.string.formatting_help_code_javascript_inline)) + " " + lineBreak +
+ getString(R.string.formatting_help_codefence_inline, getString(R.string.formatting_help_code_javascript_inline)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_code_body_2) + lineBreak +
+ lineBreak +
+ outerCodefence + lineBreak +
+ codefence + lineBreak +
+ javascript +
+ codefence + lineBreak +
+ outerCodefence + lineBreak +
+ lineBreak +
+ codefence + lineBreak +
+ javascript +
+ codefence + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_code_body_3) + lineBreak +
+ lineBreak +
+ outerCodefence + lineBreak +
+ getString(R.string.formatting_help_codefence_javascript) + lineBreak +
+ javascript +
+ codefence + lineBreak +
+ outerCodefence + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_codefence_javascript) + lineBreak +
+ javascript +
+ codefence + lineBreak +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_tables_title)) + lineBreak +
+ lineBreak +
+ codefence + lineBreak +
+ table +
+ codefence + lineBreak +
+ lineBreak +
+ table +
+ lineBreak +
+ divider + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_title, getString(R.string.formatting_help_images_title)) + lineBreak +
+ lineBreak +
+ getString(R.string.formatting_help_images_body_1, getString(R.string.formatting_help_codefence_inline, getString(R.string.formatting_help_images_slash))) + lineBreak +
+ getString(R.string.formatting_help_images_body_2, getString(R.string.formatting_help_codefence_inline, getString(R.string.formatting_help_images_escaped_space))) + lineBreak +
+ lineBreak +
+ codefence + lineBreak +
+ getString(R.string.formatting_help_image, getString(R.string.formatting_help_images_alt), getString(R.string.formatting_help_images_escaped_space)) + lineBreak +
+ codefence + lineBreak;
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..f5695a408c2d680b15807830fc938222329c45f7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/LockedActivity.java
@@ -0,0 +1,112 @@
+package it.niedermann.owncloud.notes;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import it.niedermann.owncloud.notes.branding.BrandedActivity;
+import it.niedermann.owncloud.notes.exception.ExceptionHandler;
+
+public abstract class LockedActivity extends BrandedActivity {
+
+ private static final String TAG = LockedActivity.class.getSimpleName();
+
+ private static final int REQUEST_CODE_UNLOCK = 100;
+
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_prevent_screen_capture), false)) {
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
+ }
+
+ if (isTaskRoot()) {
+ askToUnlock();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (!isTaskRoot()) {
+ askToUnlock();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (isTaskRoot()) {
+ NotesApplication.updateLastInteraction();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ NotesApplication.updateLastInteraction();
+ }
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {
+ NotesApplication.updateLastInteraction();
+ super.startActivityForResult(intent, requestCode, options);
+ }
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode) {
+ NotesApplication.updateLastInteraction();
+ super.startActivityForResult(intent, requestCode);
+ }
+
+ @Override
+ public void startActivity(Intent intent) {
+ NotesApplication.updateLastInteraction();
+ super.startActivity(intent);
+ }
+
+ @Override
+ public void startActivity(Intent intent, @Nullable Bundle options) {
+ NotesApplication.updateLastInteraction();
+ super.startActivity(intent, options);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == REQUEST_CODE_UNLOCK) {
+ if (resultCode == RESULT_OK) {
+ Log.v(TAG, "Successfully unlocked device");
+ NotesApplication.unlock();
+ } else {
+ Log.e(TAG, "Result code of unlocking was " + resultCode);
+ finish();
+ }
+ }
+ }
+
+ private void askToUnlock() {
+ if (NotesApplication.isLocked()) {
+ final var keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+ if (keyguardManager != null) {
+ final var intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.unlock_notes), null);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ startActivityForResult(intent, REQUEST_CODE_UNLOCK);
+ } else {
+ Log.e(TAG, "Keyguard manager is null");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..e177f7c7c9ecada189cce84e6aa2f4498fe69d6d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
@@ -0,0 +1,92 @@
+package it.niedermann.owncloud.notes;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.util.Log;
+
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.preference.PreferenceManager;
+
+import it.niedermann.owncloud.notes.preferences.DarkModeSetting;
+
+import static androidx.preference.PreferenceManager.getDefaultSharedPreferences;
+
+public class NotesApplication extends Application {
+ private static final String TAG = NotesApplication.class.getSimpleName();
+
+ private static final long LOCK_TIME = 30_000;
+ private static boolean lockedPreference = false;
+ private static boolean isLocked = true;
+ private static long lastInteraction = 0;
+ private static String PREF_KEY_THEME;
+ private static boolean isGridViewEnabled = false;
+
+ @Override
+ public void onCreate() {
+ PREF_KEY_THEME = getString(R.string.pref_key_theme);
+ setAppTheme(getAppTheme(getApplicationContext()));
+ final var prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false);
+ isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false);
+ super.onCreate();
+ }
+
+ public static void setAppTheme(DarkModeSetting setting) {
+ AppCompatDelegate.setDefaultNightMode(setting.getModeId());
+ }
+
+ public static boolean isGridViewEnabled() {
+ return isGridViewEnabled;
+ }
+
+ public static void updateGridViewEnabled(boolean gridView) {
+ isGridViewEnabled = gridView;
+ }
+
+ public static DarkModeSetting getAppTheme(Context context) {
+ final var prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String mode;
+ try {
+ mode = prefs.getString(PREF_KEY_THEME, DarkModeSetting.SYSTEM_DEFAULT.name());
+ } catch (ClassCastException e) {
+ final boolean darkModeEnabled = prefs.getBoolean(PREF_KEY_THEME, false);
+ mode = darkModeEnabled ? DarkModeSetting.DARK.name() : DarkModeSetting.LIGHT.name();
+ }
+ return DarkModeSetting.valueOf(mode);
+ }
+
+ public static boolean isDarkThemeActive(Context context, DarkModeSetting setting) {
+ if (setting == DarkModeSetting.SYSTEM_DEFAULT) {
+ return isDarkThemeActive(context);
+ } else {
+ return setting == DarkModeSetting.DARK;
+ }
+ }
+
+ public static boolean isDarkThemeActive(Context context) {
+ final int uiMode = context.getResources().getConfiguration().uiMode;
+ return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
+ }
+
+ public static void setLockedPreference(boolean lockedPreference) {
+ Log.i(TAG, "New locked preference: " + lockedPreference);
+ NotesApplication.lockedPreference = lockedPreference;
+ }
+
+ public static boolean isLocked() {
+ if (!isLocked && System.currentTimeMillis() > (LOCK_TIME + lastInteraction)) {
+ isLocked = true;
+ }
+ return lockedPreference && isLocked;
+ }
+
+ public static void unlock() {
+ isLocked = false;
+ }
+
+ public static void updateLastInteraction() {
+ lastInteraction = System.currentTimeMillis();
+ }
+}
diff --git a/app/src/main/java/foundation/e/notes/android/activity/SplashscreenActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java
similarity index 67%
rename from app/src/main/java/foundation/e/notes/android/activity/SplashscreenActivity.java
rename to app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java
index 3b62f382ba5fd75670565af153f48443a8209c65..8b72b22651f9cb5d1d1e8acee5ab024ea40fab2a 100644
--- a/app/src/main/java/foundation/e/notes/android/activity/SplashscreenActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/SplashscreenActivity.java
@@ -1,11 +1,13 @@
-package foundation.e.notes.android.activity;
+package it.niedermann.owncloud.notes;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
-import foundation.e.notes.util.ExceptionHandler;
+import it.niedermann.owncloud.notes.exception.ExceptionHandler;
+import it.niedermann.owncloud.notes.main.MainActivity;
+
/**
* Created by stefan on 18.04.17.
@@ -17,8 +19,8 @@ public class SplashscreenActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
- Intent intent = new Intent(this, NotesListViewActivity.class);
+ final var intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c5bf519f4baeada3e4285487fce1cebb9af3ca3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutActivity.java
@@ -0,0 +1,95 @@
+package it.niedermann.owncloud.notes.about;
+
+import android.os.Bundle;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import com.google.android.material.tabs.TabLayoutMediator;
+
+import it.niedermann.owncloud.notes.LockedActivity;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.ActivityAboutBinding;
+
+public class AboutActivity extends LockedActivity {
+
+ private ActivityAboutBinding binding;
+ private final static int POS_CREDITS = 0;
+ private final static int POS_CONTRIB = 1;
+ private final static int POS_LICENSE = 2;
+ private final static int TOTAL_COUNT = 3;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ binding = ActivityAboutBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setSupportActionBar(binding.toolbar);
+ binding.pager.setAdapter(new TabsStateAdapter(this));
+ // generate title based on given position
+ new TabLayoutMediator(binding.tabs, binding.pager, (tab, position) -> {
+ switch (position) {
+ default: // Fall-through to credits tab
+ case POS_CREDITS:
+ tab.setText(R.string.about_credits_tab_title);
+ break;
+ case POS_CONTRIB:
+ tab.setText(R.string.about_contribution_tab_title);
+ break;
+ case POS_LICENSE:
+ tab.setText(R.string.about_license_tab_title);
+ break;
+ }
+ }).attach();
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar);
+ @ColorInt int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(this, mainColor);
+ binding.tabs.setSelectedTabIndicatorColor(finalMainColor);
+ }
+
+ private static class TabsStateAdapter extends FragmentStateAdapter {
+
+ TabsStateAdapter(FragmentActivity fa) {
+ super(fa);
+ }
+
+ @Override
+ public int getItemCount() {
+ return TOTAL_COUNT;
+ }
+
+ /**
+ * return the right fragment for the given position
+ */
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ default: // Fall-through to credits tab
+ case POS_CREDITS:
+ return new AboutFragmentCreditsTab();
+
+ case POS_CONTRIB:
+ return new AboutFragmentContributingTab();
+
+ case POS_LICENSE:
+ return new AboutFragmentLicenseTab();
+ }
+ }
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish(); // close this activity as oppose to navigating up
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java
new file mode 100644
index 0000000000000000000000000000000000000000..90a9a3bce964ec120d941bb54c176f4965ee421e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentContributingTab.java
@@ -0,0 +1,28 @@
+package it.niedermann.owncloud.notes.about;
+
+import static it.niedermann.owncloud.notes.shared.util.SupportUtil.setTextWithURL;
+
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.FragmentAboutContributionTabBinding;
+import it.niedermann.owncloud.notes.shared.util.SupportUtil;
+
+public class AboutFragmentContributingTab extends Fragment {
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final var binding = FragmentAboutContributionTabBinding.inflate(inflater, container, false);
+ setTextWithURL(binding.aboutSource, getResources(), R.string.about_source, R.string.url_source, R.string.url_source);
+ setTextWithURL(binding.aboutIssues, getResources(), R.string.about_issues, R.string.url_issues, R.string.url_issues);
+ setTextWithURL(binding.aboutTranslate, getResources(), R.string.about_translate, R.string.url_translations, R.string.url_translations);
+ return binding.getRoot();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java
new file mode 100644
index 0000000000000000000000000000000000000000..028bfa4e0a95e6e9534a725163a2f30fdcc18dea
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentCreditsTab.java
@@ -0,0 +1,31 @@
+package it.niedermann.owncloud.notes.about;
+
+import static it.niedermann.owncloud.notes.shared.util.SupportUtil.setTextWithURL;
+import static it.niedermann.owncloud.notes.shared.util.SupportUtil.strong;
+import static it.niedermann.owncloud.notes.shared.util.SupportUtil.url;
+
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import it.niedermann.owncloud.notes.BuildConfig;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.FragmentAboutCreditsTabBinding;
+
+public class AboutFragmentCreditsTab extends Fragment {
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final var binding = FragmentAboutCreditsTabBinding.inflate(inflater, container, false);
+ binding.aboutVersion.setText(getString(R.string.about_version, strong(BuildConfig.VERSION_NAME)));
+ binding.aboutMaintainer.setText(url(getString(R.string.about_maintainer), getString(R.string.url_maintainer)));
+ binding.aboutMaintainer.setMovementMethod(new LinkMovementMethod());
+ setTextWithURL(binding.aboutTranslators, getResources(), R.string.about_translators_transifex, R.string.about_translators_transifex_label, R.string.url_translations);
+ return binding.getRoot();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java
new file mode 100644
index 0000000000000000000000000000000000000000..10cc02c74afe814326a97375c3cfa94b731a5deb
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/about/AboutFragmentLicenseTab.java
@@ -0,0 +1,42 @@
+package it.niedermann.owncloud.notes.about;
+
+import static it.niedermann.owncloud.notes.shared.util.SupportUtil.setTextWithURL;
+
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.core.graphics.drawable.DrawableCompat;
+
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedFragment;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.FragmentAboutLicenseTabBinding;
+
+public class AboutFragmentLicenseTab extends BrandedFragment {
+
+ private FragmentAboutLicenseTabBinding binding;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ binding = FragmentAboutLicenseTabBinding.inflate(inflater, container, false);
+ setTextWithURL(binding.aboutIconsDisclaimerAppIcon, getResources(), R.string.about_icons_disclaimer_app_icon, R.string.about_app_icon_author_link_label, R.string.url_about_icon_author);
+ setTextWithURL(binding.aboutIconsDisclaimerMdiIcons, getResources(), R.string.about_icons_disclaimer_mdi_icons, R.string.about_icons_disclaimer_mdi, R.string.url_about_icons_disclaimer_mdi);
+ binding.aboutAppLicenseButton.setOnClickListener((v) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_license)))));
+ return binding.getRoot();
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ @ColorInt final int finalMainColor = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor);
+ DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor));
+ binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..56350be74177b123388165b01c6ca794b74200ab
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerDialogFragment.java
@@ -0,0 +1,117 @@
+package it.niedermann.owncloud.notes.accountpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder;
+import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
+import it.niedermann.owncloud.notes.databinding.DialogChooseAccountBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.account.AccountChooserAdapter;
+import it.niedermann.owncloud.notes.shared.account.AccountChooserViewHolder;
+
+/**
+ * A {@link DialogFragment} which provides an {@link Account} chooser that hides the current {@link Account}.
+ * This can be useful when one wants to pick e. g. a target for move a {@link Note} from one {@link Account} to another..
+ */
+public class AccountPickerDialogFragment extends BrandedDialogFragment {
+
+ private static final String PARAM_TARGET_ACCOUNTS = "targetAccounts";
+ private static final String PARAM_CURRENT_ACCOUNT_ID = "currentAccountId";
+
+ private AccountPickerListener accountPickerListener;
+
+ private List targetAccounts;
+
+ /**
+ * Use newInstance()-Method
+ */
+ public AccountPickerDialogFragment() {
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof AccountPickerListener) {
+ this.accountPickerListener = (AccountPickerListener) context;
+ } else {
+ throw new ClassCastException("Caller must implement " + AccountPickerListener.class.getSimpleName());
+ }
+ final var args = requireArguments();
+ if (!args.containsKey(PARAM_TARGET_ACCOUNTS)) {
+ throw new IllegalArgumentException(PARAM_TARGET_ACCOUNTS + " is required.");
+ }
+ final var accounts = (Collection>) args.getSerializable(PARAM_TARGET_ACCOUNTS);
+ if (accounts == null) {
+ throw new IllegalArgumentException(PARAM_TARGET_ACCOUNTS + " is required.");
+ }
+ final long currentAccountId = requireArguments().getLong(PARAM_CURRENT_ACCOUNT_ID, -1L);
+ targetAccounts = accounts
+ .stream()
+ .map(a -> (Account) a)
+ .filter(a -> a.getId() != currentAccountId)
+ .collect(Collectors.toList());
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final var dialogBuilder = new BrandedAlertDialogBuilder(requireActivity())
+ .setTitle(R.string.simple_move)
+ .setNegativeButton(android.R.string.cancel, null);
+
+ if (targetAccounts.size() > 0) {
+ final var binding = DialogChooseAccountBinding.inflate(LayoutInflater.from(requireContext()));
+ final var adapter = new AccountChooserAdapter(targetAccounts, (account -> {
+ accountPickerListener.onAccountPicked(account);
+ dismiss();
+ }));
+ binding.accountsList.setAdapter(adapter);
+ dialogBuilder.setView(binding.getRoot());
+ } else {
+ dialogBuilder.setMessage(getString(R.string.no_other_accounts));
+ }
+
+ return dialogBuilder.create();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ Objects.requireNonNull(requireDialog().getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ public static DialogFragment newInstance(@NonNull ArrayList targetAccounts, long currentAccountId) {
+ final var fragment = new AccountPickerDialogFragment();
+ final var args = new Bundle();
+ args.putSerializable(PARAM_TARGET_ACCOUNTS, targetAccounts);
+ args.putLong(PARAM_CURRENT_ACCOUNT_ID, currentAccountId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ // Nothing to do...
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b77333a3ebb8eeb6800fcc52d2288f4c47ad868
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountpicker/AccountPickerListener.java
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.accountpicker;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+public interface AccountPickerListener {
+ void onAccountPicked(@NonNull Account account);
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..becc3ac1e85a90595f4c6b4ae7a0b79f43050fb2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java
@@ -0,0 +1,54 @@
+package it.niedermann.owncloud.notes.accountswitcher;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+public class AccountSwitcherAdapter extends RecyclerView.Adapter {
+
+ @NonNull
+ private final List localAccounts = new ArrayList<>();
+ @NonNull
+ private final Consumer onAccountClick;
+
+ public AccountSwitcherAdapter(@NonNull Consumer onAccountClick) {
+ this.onAccountClick = onAccountClick;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return localAccounts.get(position).getId();
+ }
+
+ @NonNull
+ @Override
+ public AccountSwitcherViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new AccountSwitcherViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_account_choose, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AccountSwitcherViewHolder holder, int position) {
+ holder.bind(localAccounts.get(position), onAccountClick);
+ }
+
+ @Override
+ public int getItemCount() {
+ return localAccounts.size();
+ }
+
+ public void setLocalAccounts(@NonNull List localAccounts) {
+ this.localAccounts.clear();
+ this.localAccounts.addAll(localAccounts);
+ notifyDataSetChanged();
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..b15f6308c77eb6c054479c3f0cc1659bca1dc694
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java
@@ -0,0 +1,126 @@
+package it.niedermann.owncloud.notes.accountswitcher;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.LayerDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.LiveData;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
+import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding;
+import it.niedermann.owncloud.notes.manageaccounts.ManageAccountsActivity;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.applyBrandToLayerDrawable;
+
+/**
+ * Displays all available {@link Account} entries and provides basic operations for them, like adding or switching
+ */
+public class AccountSwitcherDialog extends BrandedDialogFragment {
+
+ private static final String KEY_CURRENT_ACCOUNT_ID = "current_account_id";
+
+ private NotesRepository repo;
+ private DialogAccountSwitcherBinding binding;
+ private AccountSwitcherListener accountSwitcherListener;
+ private long currentAccountId;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof AccountSwitcherListener) {
+ this.accountSwitcherListener = (AccountSwitcherListener) context;
+ } else {
+ throw new ClassCastException("Caller must implement " + AccountSwitcherListener.class.getSimpleName());
+ }
+
+ final var args = getArguments();
+
+ if (args == null || !args.containsKey(KEY_CURRENT_ACCOUNT_ID)) {
+ throw new IllegalArgumentException("Please provide at least " + KEY_CURRENT_ACCOUNT_ID);
+ } else {
+ this.currentAccountId = args.getLong(KEY_CURRENT_ACCOUNT_ID);
+ }
+
+ repo = NotesRepository.getInstance(requireContext());
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
+
+ final var account$ = repo.getAccountById$(currentAccountId);
+ account$.observe(requireActivity(), (currentLocalAccount) -> {
+ account$.removeObservers(requireActivity());
+
+ 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")
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.currentAccountItemAvatar);
+ binding.accountLayout.setOnClickListener((v) -> dismiss());
+
+ final var adapter = new AccountSwitcherAdapter((localAccount -> {
+ accountSwitcherListener.onAccountChosen(localAccount);
+ dismiss();
+ }));
+ binding.accountsList.setAdapter(adapter);
+ final var localAccounts$ = repo.getAccounts$();
+ localAccounts$.observe(requireActivity(), (localAccounts) -> {
+ localAccounts$.removeObservers(requireActivity());
+ for (final var localAccount : localAccounts) {
+ if (localAccount.getId() == currentLocalAccount.getId()) {
+ localAccounts.remove(localAccount);
+ break;
+ }
+ }
+ adapter.setLocalAccounts(localAccounts);
+ });
+ });
+
+ binding.addAccount.setOnClickListener((v) -> {
+ accountSwitcherListener.addAccount();
+ dismiss();
+ });
+
+ binding.manageAccounts.setOnClickListener((v) -> {
+ requireActivity().startActivity(new Intent(requireContext(), ManageAccountsActivity.class));
+ dismiss();
+ });
+
+ return new AlertDialog.Builder(requireContext())
+ .setView(binding.getRoot())
+ .create();
+ }
+
+ public static DialogFragment newInstance(long currentAccountId) {
+ final var dialog = new AccountSwitcherDialog();
+
+ final var args = new Bundle();
+ args.putLong(KEY_CURRENT_ACCOUNT_ID, currentAccountId);
+ dialog.setArguments(args);
+
+ return dialog;
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ applyBrandToLayerDrawable((LayerDrawable) binding.check.getDrawable(), R.id.area, mainColor);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..87491a163d797658d9f92e35e47b8349bcc9991b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherListener.java
@@ -0,0 +1,11 @@
+package it.niedermann.owncloud.notes.accountswitcher;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+public interface AccountSwitcherListener {
+ void addAccount();
+
+ void onAccountChosen(@NonNull Account localAccount);
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f096c96e05e38fe378ba07c04437d71200fe615
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java
@@ -0,0 +1,39 @@
+package it.niedermann.owncloud.notes.accountswitcher;
+
+import android.net.Uri;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemAccountChooseBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+
+public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
+
+ ItemAccountChooseBinding binding;
+
+ public AccountSwitcherViewHolder(@NonNull View itemView) {
+ super(itemView);
+ binding = ItemAccountChooseBinding.bind(itemView);
+ }
+
+ public void bind(@NonNull Account localAccount, @NonNull Consumer onAccountClick) {
+ binding.accountName.setText(localAccount.getDisplayName());
+ binding.accountHost.setText(Uri.parse(localAccount.getUrl()).getHost());
+ Glide.with(itemView.getContext())
+ .load(new SingleSignOnUrl(localAccount.getAccountName(), localAccount.getUrl() + "/index.php/avatar/" + Uri.encode(localAccount.getUserName()) + "/64"))
+ .placeholder(R.drawable.ic_account_circle_grey_24dp)
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.accountItemAvatar);
+ itemView.setOnClickListener((v) -> onAccountClick.accept(localAccount));
+ binding.accountContextMenu.setVisibility(View.GONE);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/Branded.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/Branded.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ef9138dea8ec8bcfcb8c3cdff6ac80936fc8991
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/Branded.java
@@ -0,0 +1,9 @@
+package it.niedermann.owncloud.notes.branding;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.UiThread;
+
+public interface Branded {
+ @UiThread
+ void applyBrand(@ColorInt int mainColor, @ColorInt int textColor);
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7cc6dfd8cf71cfe52a8d13407b0f51f53aec4c6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedActivity.java
@@ -0,0 +1,68 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.Menu;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import it.niedermann.owncloud.notes.R;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.readBrandColors;
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon;
+
+public abstract class BrandedActivity extends AppCompatActivity implements Branded {
+
+ @ColorInt
+ protected int colorAccent;
+
+ public static void applyBrandToFAB(@ColorInt int mainColor, @ColorInt int textColor, @NonNull FloatingActionButton fab) {
+ fab.setSupportBackgroundTintList(ColorStateList.valueOf(mainColor));
+ fab.setColorFilter(textColor);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ final var typedValue = new TypedValue();
+ getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true);
+ colorAccent = typedValue.data;
+
+ readBrandColors(this).observe(this, (pair) -> applyBrand(pair.first, pair.second));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ for (int i = 0; i < menu.size(); i++) {
+ tintMenuIcon(menu.getItem(i), colorAccent);
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ public void applyBrandToPrimaryToolbar(@NonNull AppBarLayout appBarLayout, @NonNull Toolbar toolbar) {
+ // FIXME Workaround for https://github.com/stefan-niedermann/nextcloud-notes/issues/889
+ appBarLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.primary));
+
+ final var overflowDrawable = toolbar.getOverflowIcon();
+ if (overflowDrawable != null) {
+ overflowDrawable.setColorFilter(colorAccent, PorterDuff.Mode.SRC_ATOP);
+ toolbar.setOverflowIcon(overflowDrawable);
+ }
+
+ final var navigationDrawable = toolbar.getNavigationIcon();
+ if (navigationDrawable != null) {
+ navigationDrawable.setColorFilter(colorAccent, PorterDuff.Mode.SRC_ATOP);
+ toolbar.setNavigationIcon(navigationDrawable);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedAlertDialogBuilder.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedAlertDialogBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..c48e62981a9db4917d08911a2b1cd3fe7d8d2f96
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedAlertDialogBuilder.java
@@ -0,0 +1,48 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Button;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+
+public class BrandedAlertDialogBuilder extends AlertDialog.Builder implements Branded {
+
+ protected AlertDialog dialog;
+
+ public BrandedAlertDialogBuilder(Context context) {
+ super(context);
+ }
+
+ @NonNull
+ @Override
+ public AlertDialog create() {
+ this.dialog = super.create();
+
+ @NonNull final var context = getContext();
+ @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
+ @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
+ applyBrand(mainColor, textColor);
+ dialog.setOnShowListener(dialog -> applyBrand(mainColor, textColor));
+ return dialog;
+ }
+
+ @CallSuper
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ final var buttons = new Button[3];
+ buttons[0] = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ buttons[1] = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
+ buttons[2] = dialog.getButton(DialogInterface.BUTTON_NEUTRAL);
+ for (final var button : buttons) {
+ if (button != null) {
+ button.setTextColor(getSecondaryForegroundColorDependingOnTheme(button.getContext(), mainColor));
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDeleteAlertDialogBuilder.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDeleteAlertDialogBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..e50b6fcba26e8e52b68d1229e32ffbc480d4ae72
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDeleteAlertDialogBuilder.java
@@ -0,0 +1,26 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Button;
+
+import androidx.annotation.CallSuper;
+
+import it.niedermann.owncloud.notes.R;
+
+public class BrandedDeleteAlertDialogBuilder extends BrandedAlertDialogBuilder {
+
+ public BrandedDeleteAlertDialogBuilder(Context context) {
+ super(context);
+ }
+
+ @CallSuper
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ super.applyBrand(mainColor, textColor);
+ final var positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ if (positiveButton != null) {
+ positiveButton.setTextColor(getContext().getResources().getColor(R.color.bg_attention));
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..b930c63a9d0c6e76638f8e07b9517b1063e2a69d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedDialogFragment.java
@@ -0,0 +1,20 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+public abstract class BrandedDialogFragment extends DialogFragment implements Branded {
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ @Nullable final var context = requireContext();
+ @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
+ @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
+ applyBrand(mainColor, textColor);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..993ee37784d8ceb4698a34e38b383e618b7b640e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedFragment.java
@@ -0,0 +1,47 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuInflater;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import it.niedermann.owncloud.notes.R;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon;
+
+public abstract class BrandedFragment extends Fragment implements Branded {
+
+ @ColorInt
+ protected int colorAccent;
+ @ColorInt
+ protected int colorPrimary;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ final var context = requireContext();
+ final var typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true);
+ colorAccent = typedValue.data;
+ context.getTheme().resolveAttribute(R.attr.colorPrimary, typedValue, true);
+ colorPrimary = typedValue.data;
+
+ @ColorInt final int mainColor = BrandingUtil.readBrandMainColor(context);
+ @ColorInt final int textColor = BrandingUtil.readBrandTextColor(context);
+ applyBrand(mainColor, textColor);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ for (int i = 0; i < menu.size(); i++) {
+ tintMenuIcon(menu.getItem(i), colorAccent);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java
new file mode 100644
index 0000000000000000000000000000000000000000..620ec4b669c614a2698af63d865b5f3ba8021e9d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedPreferenceCategory.java
@@ -0,0 +1,44 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceViewHolder;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+
+public class BrandedPreferenceCategory extends PreferenceCategory {
+
+ public BrandedPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BrandedPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BrandedPreferenceCategory(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BrandedPreferenceCategory(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ final var view = holder.itemView.findViewById(android.R.id.title);
+ @Nullable final var context = getContext();
+ if (context != null && view instanceof TextView) {
+ @ColorInt final int mainColor = getSecondaryForegroundColorDependingOnTheme(context, BrandingUtil.readBrandMainColor(context));
+ ((TextView) view).setTextColor(mainColor);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e3a4d9fe93548c869137a15ef6905e024b245b1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSnackbar.java
@@ -0,0 +1,28 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.graphics.Color;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import it.niedermann.android.util.ColorUtil;
+
+public class BrandedSnackbar {
+
+ @NonNull
+ public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Snackbar.Duration int duration) {
+ final var snackbar = Snackbar.make(view, text, duration);
+ final int color = BrandingUtil.readBrandMainColor(view.getContext());
+ snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color);
+ return snackbar;
+ }
+
+ @NonNull
+ public static Snackbar make(@NonNull View view, @StringRes int resId, @Snackbar.Duration int duration) {
+ return make(view, view.getResources().getText(resId), duration);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java
new file mode 100644
index 0000000000000000000000000000000000000000..07eb0c63c632e1988ddf8894eb0ccab30a90a4d5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedSwitchPreference.java
@@ -0,0 +1,111 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Switch;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.preference.PreferenceViewHolder;
+import androidx.preference.SwitchPreference;
+
+import it.niedermann.owncloud.notes.R;
+
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+
+public class BrandedSwitchPreference extends SwitchPreference implements Branded {
+
+ @ColorInt
+ private Integer mainColor = null;
+
+ @ColorInt
+ private Integer textColor = null;
+
+ @SuppressLint("UseSwitchCompatOrMaterialCode")
+ @Nullable
+ private Switch switchView;
+
+ public BrandedSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public BrandedSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public BrandedSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public BrandedSwitchPreference(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ if (holder.itemView instanceof ViewGroup) {
+ switchView = findSwitchWidget(holder.itemView);
+ if (mainColor != null && textColor != null) {
+ applyBrand();
+ }
+ }
+ }
+
+ @Override
+ public void applyBrand(@ColorInt int mainColor, @ColorInt int textColor) {
+ this.mainColor = mainColor;
+ this.textColor = textColor;
+ // onBindViewHolder is called after applyBrand, therefore we have to store the given values and apply them later.
+ applyBrand();
+ }
+
+ private void applyBrand() {
+ if (switchView != null) {
+ final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(getContext(), mainColor);
+ // int trackColor = Color.argb(77, Color.red(finalMainColor), Color.green(finalMainColor), Color.blue(finalMainColor));
+ DrawableCompat.setTintList(switchView.getThumbDrawable(), new ColorStateList(
+ new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
+ new int[]{finalMainColor, getContext().getResources().getColor(R.color.fg_default_low)}
+ ));
+ DrawableCompat.setTintList(switchView.getTrackDrawable(), new ColorStateList(
+ new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
+ new int[]{finalMainColor, getContext().getResources().getColor(R.color.fg_default_low)}
+ ));
+ }
+ }
+
+ /**
+ * Recursively go through view tree until we find an android.widget.Switch
+ *
+ * @param view Root view to start searching
+ * @return A Switch class or null
+ * @see Source
+ */
+ private Switch findSwitchWidget(View view) {
+ if (view instanceof Switch) {
+ return (Switch) view;
+ }
+ if (view instanceof ViewGroup) {
+ final var viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ final var child = viewGroup.getChildAt(i);
+ if (child instanceof ViewGroup) {
+ @SuppressLint("UseSwitchCompatOrMaterialCode") final var result = findSwitchWidget(child);
+ if (result != null) return result;
+ }
+ if (child instanceof Switch) {
+ return (Switch) child;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..af2b4a43c61aba218e084fc98300bf2b6fdacb42
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandingUtil.java
@@ -0,0 +1,164 @@
+package it.niedermann.owncloud.notes.branding;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.Log;
+import android.view.MenuItem;
+import android.widget.EditText;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.preference.PreferenceManager;
+
+import it.niedermann.android.sharedpreferences.SharedPreferenceIntLiveData;
+import it.niedermann.owncloud.notes.NotesApplication;
+import it.niedermann.owncloud.notes.R;
+
+import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
+
+public class BrandingUtil {
+
+ private static final String TAG = BrandingUtil.class.getSimpleName();
+ private static final String pref_key_branding_main = "branding_main";
+ private static final String pref_key_branding_text = "branding_text";
+
+ private BrandingUtil() {
+
+ }
+
+ public static LiveData> readBrandColors(@NonNull Context context) {
+ return new BrandingLiveData(context);
+ }
+
+ private static class BrandingLiveData extends MediatorLiveData> {
+ @ColorInt
+ Integer lastMainColor = null;
+ @ColorInt
+ Integer lastTextColor = null;
+
+ public BrandingLiveData(@NonNull Context context) {
+ addSource(readBrandMainColorLiveData(context), (nextMainColor) -> {
+ lastMainColor = nextMainColor;
+ if (lastTextColor != null) {
+ postValue(new Pair<>(lastMainColor, lastTextColor));
+ }
+ });
+ addSource(readBrandTextColorLiveData(context), (nextTextColor) -> {
+ lastTextColor = nextTextColor;
+ if (lastMainColor != null) {
+ postValue(new Pair<>(lastMainColor, lastTextColor));
+ }
+ });
+ }
+ }
+
+ public static LiveData readBrandMainColorLiveData(@NonNull Context context) {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_main");
+ return new SharedPreferenceIntLiveData(sharedPreferences, pref_key_branding_main, context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
+ }
+
+ public static LiveData readBrandTextColorLiveData(@NonNull Context context) {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_text");
+ return new SharedPreferenceIntLiveData(sharedPreferences, pref_key_branding_text, Color.WHITE);
+ }
+
+ @ColorInt
+ public static int readBrandMainColor(@NonNull Context context) {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_main");
+ return sharedPreferences.getInt(pref_key_branding_main, context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
+ }
+
+ @ColorInt
+ public static int readBrandTextColor(@NonNull Context context) {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ Log.v(TAG, "--- Read: shared_preference_theme_text");
+ return sharedPreferences.getInt(pref_key_branding_text, Color.WHITE);
+ }
+
+ public static void saveBrandColors(@NonNull Context context, @ColorInt int mainColor, @ColorInt int textColor) {
+ final int previousMainColor = readBrandMainColor(context);
+ final int previousTextColor = readBrandTextColor(context);
+ final var editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
+ Log.v(TAG, "--- Write: shared_preference_theme_main" + " | " + mainColor);
+ Log.v(TAG, "--- Write: shared_preference_theme_text" + " | " + textColor);
+ editor.putInt(pref_key_branding_main, mainColor);
+ editor.putInt(pref_key_branding_text, textColor);
+ editor.apply();
+ if (context instanceof BrandedActivity) {
+ if (mainColor != previousMainColor || textColor != previousTextColor) {
+ final var activity = (BrandedActivity) context;
+ activity.runOnUiThread(() -> ActivityCompat.recreate(activity));
+ }
+ }
+ }
+
+ /**
+ * Since we may collide with dark theme in this area, we have to make sure that the color is visible depending on the background
+ */
+ @ColorInt
+ public static int getSecondaryForegroundColorDependingOnTheme(@NonNull Context context, @ColorInt int mainColor) {
+ final int primaryColor = ContextCompat.getColor(context, R.color.primary);
+ final boolean isDarkTheme = NotesApplication.isDarkThemeActive(context);
+ if (isDarkTheme && !contrastRatioIsSufficient(mainColor, primaryColor)) {
+ Log.v(TAG, "Contrast ratio between brand color " + String.format("#%06X", (0xFFFFFF & mainColor)) + " and dark theme is too low. Falling back to WHITE as brand color.");
+ return Color.WHITE;
+ } else if (!isDarkTheme && !contrastRatioIsSufficient(mainColor, primaryColor)) {
+ Log.v(TAG, "Contrast ratio between brand color " + String.format("#%06X", (0xFFFFFF & mainColor)) + " and light theme is too low. Falling back to BLACK as brand color.");
+ return Color.BLACK;
+ } else {
+ return mainColor;
+ }
+ }
+
+ public static void applyBrandToEditText(@ColorInt int mainColor, @ColorInt int textColor, @NonNull EditText editText) {
+ @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(editText.getContext(), mainColor);
+ DrawableCompat.setTintList(editText.getBackground(), new ColorStateList(
+ new int[][]{
+ new int[]{android.R.attr.state_active},
+ new int[]{android.R.attr.state_activated},
+ new int[]{android.R.attr.state_focused},
+ new int[]{android.R.attr.state_pressed},
+ new int[]{}
+ },
+ new int[]{
+ finalMainColor,
+ finalMainColor,
+ finalMainColor,
+ finalMainColor,
+ editText.getContext().getResources().getColor(R.color.fg_default_low)
+ }
+ ));
+ }
+
+ public static void tintMenuIcon(@NonNull MenuItem menuItem, @ColorInt int color) {
+ var drawable = menuItem.getIcon();
+ if (drawable != null) {
+ drawable = DrawableCompat.wrap(drawable);
+ DrawableCompat.setTint(drawable, color);
+ menuItem.setIcon(drawable);
+ }
+ }
+
+ public static void applyBrandToLayerDrawable(@NonNull LayerDrawable check, @IdRes int areaToColor, @ColorInt int mainColor) {
+ final var drawable = check.findDrawableByLayerId(areaToColor);
+ if (drawable == null) {
+ Log.e(TAG, "Could not find areaToColor (" + areaToColor + "). Cannot apply brand.");
+ } else {
+ DrawableCompat.setTint(drawable, mainColor);
+ }
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..0695300699889ea609fcd45eb9fd6ce38f02f482
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
@@ -0,0 +1,412 @@
+package it.niedermann.owncloud.notes.edit;
+
+import static java.lang.Boolean.TRUE;
+import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive;
+import static it.niedermann.owncloud.notes.branding.BrandingUtil.tintMenuIcon;
+import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
+import static it.niedermann.owncloud.notes.shared.util.WidgetUtil.pendingIntentFlagCompat;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ScrollView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+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;
+import it.niedermann.owncloud.notes.accountpicker.AccountPickerDialogFragment;
+import it.niedermann.owncloud.notes.branding.BrandedFragment;
+import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
+import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment.CategoryDialogListener;
+import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment;
+import it.niedermann.owncloud.notes.edit.title.EditTitleDialogFragment.EditTitleListener;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+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;
+
+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";
+ public static final String PARAM_ACCOUNT_ID = "accountId";
+ public static final String PARAM_CONTENT = "content";
+ public static final String PARAM_NEWNOTE = "newNote";
+ private static final String SAVEDKEY_NOTE = "note";
+ private static final String SAVEDKEY_ORIGINAL_NOTE = "original_note";
+
+ private Account localAccount;
+
+ protected Note note;
+ // TODO do we really need this? The reference to note is currently the same
+ @Nullable
+ private Note originalNote;
+ private int originalScrollY;
+ protected NotesRepository repo;
+ private NoteFragmentListener listener;
+ private boolean titleModified = false;
+
+ protected boolean isNew = true;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ try {
+ listener = (NoteFragmentListener) context;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(context.getClass() + " must implement " + NoteFragmentListener.class);
+ }
+ repo = NotesRepository.getInstance(context);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ executor.submit(() -> {
+ try {
+ final var ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext());
+ this.localAccount = repo.getAccountByName(ssoAccount.name);
+
+ if (savedInstanceState == null) {
+ final long id = requireArguments().getLong(PARAM_NOTE_ID);
+ if (id > 0) {
+ final long accountId = requireArguments().getLong(PARAM_ACCOUNT_ID);
+ if (accountId > 0) {
+ /* Switch account if account id has been provided */
+ this.localAccount = repo.getAccountById(accountId);
+ SingleAccountHelper.setCurrentAccount(requireContext().getApplicationContext(), localAccount.getAccountName());
+ }
+ isNew = false;
+ note = originalNote = repo.getNoteById(id);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ } else {
+ final var paramNote = (Note) requireArguments().getSerializable(PARAM_NEWNOTE);
+ final var content = requireArguments().getString(PARAM_CONTENT);
+ if (paramNote == null) {
+ if (content == null) {
+ throw new IllegalArgumentException(PARAM_NOTE_ID + " is not given, argument " + PARAM_NEWNOTE + " is missing and " + PARAM_CONTENT + " is missing.");
+ } else {
+ note = new Note(-1, null, Calendar.getInstance(), NoteUtil.generateNoteTitle(content), content, getString(R.string.category_readonly), false, null, DBStatus.VOID, -1, "", 0);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ }
+ } else {
+ paramNote.setStatus(DBStatus.LOCAL_EDITED);
+ note = repo.addNote(localAccount.getId(), paramNote);
+ originalNote = null;
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ }
+ }
+ } else {
+ note = (Note) savedInstanceState.getSerializable(SAVEDKEY_NOTE);
+ originalNote = (Note) savedInstanceState.getSerializable(SAVEDKEY_ORIGINAL_NOTE);
+ requireActivity().runOnUiThread(() -> onNoteLoaded(note));
+ requireActivity().invalidateOptionsMenu();
+ }
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
+ }
+ });
+ setHasOptionsMenu(true);
+ }
+
+ @Nullable
+ protected abstract ScrollView getScrollView();
+
+ protected abstract void scrollToY(int scrollY);
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ listener.onNoteUpdated(note);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ saveNote(null);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ listener = null;
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ saveNote(null);
+ outState.putSerializable(SAVEDKEY_NOTE, note);
+ outState.putSerializable(SAVEDKEY_ORIGINAL_NOTE, originalNote);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ inflater.inflate(R.menu.menu_note_fragment, menu);
+
+ if (ShortcutManagerCompat.isRequestPinShortcutSupported(requireContext()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ menu.add(Menu.NONE, MENU_ID_PIN, 110, R.string.pin_to_homescreen);
+ }
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ if (note != null) {
+ prepareFavoriteOption(menu.findItem(R.id.menu_favorite));
+
+ final var 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);
+ }
+ }
+
+ private void prepareFavoriteOption(MenuItem item) {
+ item.setIcon(TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_white_24dp : R.drawable.ic_star_border_white_24dp);
+ item.setChecked(note.getFavorite());
+ tintMenuIcon(item, colorAccent);
+ }
+
+ /**
+ * Main-Menu-Handler
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == R.id.menu_cancel) {
+ executor.submit(() -> {
+ if (originalNote == null) {
+ repo.deleteNoteAndSync(localAccount, note.getId());
+ } else {
+ repo.updateNoteAndSync(localAccount, originalNote, null, null, null);
+ }
+ });
+ listener.close();
+ return true;
+ } else if (itemId == R.id.menu_delete) {
+ repo.deleteNoteAndSync(localAccount, note.getId());
+ listener.close();
+ return true;
+ } else if (itemId == R.id.menu_favorite) {
+ repo.toggleFavoriteAndSync(localAccount, note.getId());
+ listener.onNoteUpdated(note);
+ prepareFavoriteOption(item);
+ return true;
+ } else if (itemId == R.id.menu_category) {
+ showCategorySelector();
+ return true;
+ } else if (itemId == R.id.menu_title) {
+ showEditTitleDialog();
+ return true;
+ } else if (itemId == R.id.menu_move) {
+ 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());
+ return false;
+ } else if (itemId == MENU_ID_PIN) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ final var context = requireContext();
+ if (ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
+ final var pinShortcutInfo = new ShortcutInfoCompat.Builder(context, String.valueOf(note.getId()))
+ .setShortLabel(note.getTitle())
+ .setIcon(IconCompat.createWithResource(context.getApplicationContext(), TRUE.equals(note.getFavorite()) ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp))
+ .setIntent(new Intent(getActivity(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()).setAction(ACTION_SHORTCUT))
+ .build();
+
+ ShortcutManagerCompat.requestPinShortcut(context, pinShortcutInfo, PendingIntent.getBroadcast(context, 0, ShortcutManagerCompat.createShortcutResultIntent(context, pinShortcutInfo), pendingIntentFlagCompat(0)).getIntentSender());
+ } else {
+ Log.i(TAG, "RequestPinShortcut is not supported");
+ }
+ }
+
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @CallSuper
+ protected void onNoteLoaded(Note note) {
+ this.originalScrollY = note.getScrollY();
+ scrollToY(originalScrollY);
+ final var scrollView = getScrollView();
+ if (scrollView != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ scrollView.setOnScrollChangeListener((View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) -> {
+ if (scrollY > 0) {
+ note.setScrollY(scrollY);
+ }
+ });
+ }
+ }
+ }
+
+ public void onCloseNote() {
+ if (!titleModified && originalNote == null && getContent().isEmpty()) {
+ repo.deleteNoteAndSync(localAccount, note.getId());
+ }
+ }
+
+ /**
+ * Save the current state in the database and schedule synchronization if needed.
+ *
+ * @param callback Observer which is called after save/synchronization
+ */
+ protected void saveNote(@Nullable ISyncCallback callback) {
+ Log.d(TAG, "saveData()");
+ if (note != null) {
+ final var newContent = getContent();
+ if (note.getContent().equals(newContent)) {
+ if (note.getScrollY() != originalScrollY) {
+ Log.v(TAG, "... only saving new scroll state, since content did not change");
+ repo.updateScrollY(note.getId(), note.getScrollY());
+ } else {
+ Log.v(TAG, "... not saving, since nothing has changed");
+ }
+ } else {
+ // FIXME requires database queries on main thread!
+ note = repo.updateNoteAndSync(localAccount, note, newContent, null, callback);
+ listener.onNoteUpdated(note);
+ requireActivity().invalidateOptionsMenu();
+ }
+ } else {
+ Log.e(TAG, "note is null");
+ }
+ }
+
+ protected abstract String getContent();
+
+ /**
+ * Opens a dialog in order to chose a category
+ */
+ private void showCategorySelector() {
+ final var fragmentId = "fragment_category";
+ final var manager = requireActivity().getSupportFragmentManager();
+ final var frag = manager.findFragmentByTag(fragmentId);
+ if (frag != null) {
+ manager.beginTransaction().remove(frag).commit();
+ }
+ final var categoryFragment = CategoryDialogFragment.newInstance(note.getAccountId(), note.getCategory());
+ categoryFragment.setTargetFragment(this, 0);
+ categoryFragment.show(manager, fragmentId);
+ }
+
+ /**
+ * Opens a dialog in order to chose a category
+ */
+ public void showEditTitleDialog() {
+ saveNote(null);
+ final var fragmentId = "fragment_edit_title";
+ final var manager = requireActivity().getSupportFragmentManager();
+ final var frag = manager.findFragmentByTag(fragmentId);
+ if (frag != null) {
+ manager.beginTransaction().remove(frag).commit();
+ }
+ final var editTitleFragment = EditTitleDialogFragment.newInstance(note.getTitle());
+ editTitleFragment.setTargetFragment(this, 0);
+ editTitleFragment.show(manager, fragmentId);
+ }
+
+ @Override
+ public void onCategoryChosen(String category) {
+ repo.setCategory(localAccount, note.getId(), category);
+ note.setCategory(category);
+ listener.onNoteUpdated(note);
+ }
+
+ @Override
+ public void onTitleEdited(String newTitle) {
+ titleModified = true;
+ note.setTitle(newTitle);
+ executor.submit(() -> {
+ note = repo.updateNoteAndSync(localAccount, note, note.getContent(), newTitle, null);
+ requireActivity().runOnUiThread(() -> listener.onNoteUpdated(note));
+ });
+ }
+
+ public void moveNote(Account account) {
+ final var moveLiveData = repo.moveNoteToAnotherAccount(account, note);
+ moveLiveData.observe(this, (v) -> moveLiveData.removeObservers(this));
+ listener.close();
+ }
+
+ @ColorInt
+ protected static int getTextHighlightBackgroundColor(@NonNull Context context, @ColorInt int mainColor, @ColorInt int colorPrimary, @ColorInt int colorAccent) {
+ if (isDarkThemeActive(context)) { // Dark background
+ if (ColorUtil.INSTANCE.isColorDark(mainColor)) { // Dark brand color
+ if (NotesColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text
+ return mainColor;
+ } else {
+ return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground);
+ }
+ } else { // Light brand color
+ if (NotesColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text
+ return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor));
+ } else {
+ return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground);
+ }
+ }
+ } else { // Light background
+ if (ColorUtil.INSTANCE.isColorDark(mainColor)) { // Dark brand color
+ if (NotesColorUtil.contrastRatioIsSufficient(mainColor, colorAccent)) { // But also dark text
+ return Color.argb(77, Color.red(mainColor), Color.green(mainColor), Color.blue(mainColor));
+ } else {
+ return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground);
+ }
+ } else { // Light brand color
+ if (NotesColorUtil.contrastRatioIsSufficient(mainColor, colorPrimary)) { // But also dark text
+ return mainColor;
+ } else {
+ return ContextCompat.getColor(context, R.color.defaultTextHighlightBackground);
+ }
+ }
+ }
+ }
+
+ public interface NoteFragmentListener {
+ void close();
+
+ void onNoteUpdated(Note note);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..504f333a5bca863102a1e551fc8ddbf13b65485c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
@@ -0,0 +1,298 @@
+package it.niedermann.owncloud.notes.edit;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.preference.PreferenceManager;
+
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Calendar;
+import java.util.Objects;
+
+import it.niedermann.owncloud.notes.LockedActivity;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener;
+import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
+import it.niedermann.owncloud.notes.databinding.ActivityEditBinding;
+import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
+import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+import it.niedermann.owncloud.notes.shared.util.ShareUtil;
+
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
+
+public class EditNoteActivity extends LockedActivity implements BaseNoteFragment.NoteFragmentListener, AccountPickerListener {
+
+ private static final String TAG = EditNoteActivity.class.getSimpleName();
+
+ public static final String ACTION_SHORTCUT = "it.niedermann.owncloud.notes.shortcut";
+ private static final String INTENT_GOOGLE_ASSISTANT = "com.google.android.gm.action.AUTO_SEND";
+ private static final String MIMETYPE_TEXT_PLAIN = "text/plain";
+ public static final String PARAM_NOTE_ID = "noteId";
+ public static final String PARAM_ACCOUNT_ID = "accountId";
+ public static final String PARAM_CATEGORY = "category";
+ public static final String PARAM_CONTENT = "content";
+ public static final String PARAM_FAVORITE = "favorite";
+
+ private CategoryViewModel categoryViewModel;
+ private ActivityEditBinding binding;
+
+ private BaseNoteFragment fragment;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) == null) {
+ throw new NoCurrentAccountSelectedException();
+ }
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ Toast.makeText(this, R.string.no_account_configured_yet, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ categoryViewModel = new ViewModelProvider(this).get(CategoryViewModel.class);
+ binding = ActivityEditBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+
+ if (savedInstanceState == null) {
+ launchNoteFragment();
+ } else {
+ fragment = (BaseNoteFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container_view);
+ }
+
+ setSupportActionBar(binding.toolbar);
+ binding.toolbar.setOnClickListener((v) -> fragment.showEditTitleDialog());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ Log.d(TAG, "onNewIntent: " + intent.getLongExtra(PARAM_NOTE_ID, 0));
+ setIntent(intent);
+ if (fragment != null) {
+ getSupportFragmentManager().beginTransaction().detach(fragment).commit();
+ fragment = null;
+ }
+ launchNoteFragment();
+ }
+
+ private long getNoteId() {
+ return getIntent().getLongExtra(PARAM_NOTE_ID, 0);
+ }
+
+ private long getAccountId() {
+ return getIntent().getLongExtra(PARAM_ACCOUNT_ID, 0);
+ }
+
+ /**
+ * Starts the note fragment for an existing note or a new note.
+ * The actual behavior is triggered by the activity's intent.
+ */
+ private void launchNoteFragment() {
+ long noteId = getNoteId();
+ if (noteId > 0) {
+ launchExistingNote(getAccountId(), noteId);
+ } else {
+ if (Intent.ACTION_VIEW.equals(getIntent().getAction())) {
+ launchReadonlyNote();
+ } else {
+ launchNewNote();
+ }
+ }
+ }
+
+ /**
+ * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note.
+ * The type of fragment (view-mode) is chosen based on the user preferences.
+ *
+ * @param noteId ID of the existing note.
+ */
+ private void launchExistingNote(long accountId, long noteId) {
+ final var prefKeyNoteMode = getString(R.string.pref_key_note_mode);
+ final var prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
+ final var prefValueEdit = getString(R.string.pref_value_mode_edit);
+ final var prefValuePreview = getString(R.string.pref_value_mode_preview);
+ final var prefValueLast = getString(R.string.pref_value_mode_last);
+
+ final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final String mode = preferences.getString(prefKeyNoteMode, prefValueEdit);
+ final String lastMode = preferences.getString(prefKeyLastMode, prefValueEdit);
+ boolean editMode = true;
+ if (prefValuePreview.equals(mode) || (prefValueLast.equals(mode) && prefValuePreview.equals(lastMode))) {
+ editMode = false;
+ }
+ launchExistingNote(accountId, noteId, editMode);
+ }
+
+ /**
+ * Starts a {@link NoteEditFragment} or {@link NotePreviewFragment} for an existing note.
+ *
+ * @param noteId ID of the existing note.
+ * @param edit View-mode of the fragment:
+ * true
for {@link NoteEditFragment},
+ * false
for {@link NotePreviewFragment}.
+ */
+ private void launchExistingNote(long accountId, long noteId, boolean edit) {
+ // save state of the fragment in order to resume with the same note and originalNote
+ Fragment.SavedState savedState = null;
+ if (fragment != null) {
+ savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
+ }
+ fragment = edit
+ ? NoteEditFragment.newInstance(accountId, noteId)
+ : NotePreviewFragment.newInstance(accountId, noteId);
+
+ if (savedState != null) {
+ fragment.setInitialSavedState(savedState);
+ }
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ }
+
+ /**
+ * Starts the {@link NoteEditFragment} with a new note.
+ * Content ("share" functionality), category and favorite attribute can be preset.
+ */
+ private void launchNewNote() {
+ final var intent = getIntent();
+
+ String categoryTitle = "";
+ boolean favorite = false;
+ if (intent.hasExtra(PARAM_CATEGORY)) {
+ final NavigationCategory categoryPreselection = (NavigationCategory) Objects.requireNonNull(intent.getSerializableExtra(PARAM_CATEGORY));
+ final String category = categoryPreselection.getCategory();
+ if(category != null) {
+ categoryTitle = category;
+ }
+ favorite = categoryPreselection.getType() == FAVORITES;
+ }
+
+ String content = "";
+ if (
+ intent.hasExtra(Intent.EXTRA_TEXT) &&
+ MIMETYPE_TEXT_PLAIN.equals(intent.getType()) &&
+ (Intent.ACTION_SEND.equals(intent.getAction()) ||
+ INTENT_GOOGLE_ASSISTANT.equals(intent.getAction()))
+ ) {
+ content = ShareUtil.extractSharedText(intent);
+ } else if (intent.hasExtra(PARAM_CONTENT)) {
+ content = intent.getStringExtra(PARAM_CONTENT);
+ }
+
+ if (content == null) {
+ content = "";
+ }
+ final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null);
+ fragment = NoteEditFragment.newInstanceWithNewNote(newNote);
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ }
+
+ private void launchReadonlyNote() {
+ final var intent = getIntent();
+ final var content = new StringBuilder();
+ try {
+ final var inputStream = getContentResolver().openInputStream(Objects.requireNonNull(intent.getData()));
+ final var bufferedReader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(inputStream)));
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ content.append(line).append('\n');
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ fragment = NoteReadonlyFragment.newInstance(content.toString());
+ getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container_view, fragment).commit();
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ close();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_note_activity, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ close();
+ return true;
+ } else if (itemId == R.id.menu_preview) {
+ launchExistingNote(getAccountId(), getNoteId(), false);
+ return true;
+ } else if (itemId == R.id.menu_edit) {
+ launchExistingNote(getAccountId(), getNoteId(), true);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+ /**
+ * Send result and closes the Activity
+ */
+ public void close() {
+ /* TODO enhancement: store last mode in note
+ * for cross device functionality per note mode should be stored on the server.
+ */
+ final var preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+ final String prefKeyLastMode = getString(R.string.pref_key_last_note_mode);
+ if (fragment instanceof NoteEditFragment) {
+ preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_edit)).apply();
+ } else {
+ preferences.edit().putString(prefKeyLastMode, getString(R.string.pref_value_mode_preview)).apply();
+ }
+ fragment.onCloseNote();
+ finish();
+ }
+
+ @Override
+ public void onNoteUpdated(Note note) {
+ if (note != null) {
+ binding.toolbar.setTitle(note.getTitle());
+ if (TextUtils.isEmpty(note.getCategory())) {
+ binding.toolbar.setSubtitle(null);
+ } else {
+ binding.toolbar.setSubtitle(NoteUtil.extendCategory(note.getCategory()));
+ }
+ }
+ }
+
+ @Override
+ public void onAccountPicked(@NonNull Account account) {
+ fragment.moveNote(account);
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ applyBrandToPrimaryToolbar(binding.appBar, binding.toolbar);
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..6733f4d3a36c0fd9c0ec07c800763094f1590f88
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
@@ -0,0 +1,275 @@
+package it.niedermann.owncloud.notes.edit;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ScrollView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+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;
+
+public class NoteEditFragment extends SearchableBaseNoteFragment {
+
+ private static final String TAG = NoteEditFragment.class.getSimpleName();
+
+ private static final String LOG_TAG_AUTOSAVE = "AutoSave";
+
+ private static final long DELAY = 2000; // Wait for this time after typing before saving
+ private static final long DELAY_AFTER_SYNC = 5000; // Wait for this time after saving before checking for next save
+
+ private FragmentNoteEditBinding binding;
+
+ private Handler handler;
+ private boolean saveActive;
+ private boolean unsavedEdit;
+ private final Runnable runAutoSave = new Runnable() {
+ @Override
+ public void run() {
+ if (unsavedEdit) {
+ Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: start AutoSave");
+ autoSave();
+ } else {
+ Log.d(LOG_TAG_AUTOSAVE, "runAutoSave: nothing changed");
+ }
+ }
+ };
+ private TextWatcher textWatcher;
+ private boolean keyboardShown = false;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.menu_edit).setVisible(false);
+ menu.findItem(R.id.menu_preview).setVisible(true);
+ }
+
+ @Override
+ public ScrollView getScrollView() {
+ return binding.scrollView;
+ }
+
+ @Override
+ protected void scrollToY(int y) {
+ if (binding != null) {
+ binding.scrollView.post(() -> binding.scrollView.setScrollY(y));
+ }
+ }
+
+ @Override
+ protected Layout getLayout() {
+ binding.editContent.onPreDraw();
+ return binding.editContent.getLayout();
+ }
+
+ @Override
+ protected FloatingActionButton getSearchNextButton() {
+ return binding.searchNext;
+ }
+
+ @Override
+ protected FloatingActionButton getSearchPrevButton() {
+ return binding.searchPrev;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ binding = FragmentNoteEditBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ textWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Nothing to do here...
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Nothing to do here...
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ unsavedEdit = true;
+ if (!saveActive) {
+ handler.removeCallbacks(runAutoSave);
+ handler.postDelayed(runAutoSave, DELAY);
+ }
+ }
+ };
+ }
+
+ @Override
+ 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())) {
+ openSoftKeyboard();
+ }
+
+ binding.editContent.setMarkdownString(note.getContent());
+ binding.editContent.setEnabled(true);
+
+ final var sp = PreferenceManager.getDefaultSharedPreferences(requireContext().getApplicationContext());
+ binding.editContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp));
+ if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
+ binding.editContent.setTypeface(Typeface.MONOSPACE);
+ }
+ }
+
+ private void openSoftKeyboard() {
+ binding.editContent.postDelayed(() -> {
+ binding.editContent.requestFocus();
+
+ final var 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() {
+ handler.removeCallbacks(runAutoSave);
+ }
+
+ /**
+ * Gets the current content of the EditText field in the UI.
+ *
+ * @return String of the current content.
+ */
+ @Override
+ protected String getContent() {
+ final var editable = binding.editContent.getText();
+ return editable == null ? "" : editable.toString();
+ }
+
+ @Override
+ protected void saveNote(@Nullable ISyncCallback callback) {
+ super.saveNote(callback);
+ unsavedEdit = false;
+ }
+
+ /**
+ * Saves the current changes and show the status in the ActionBar
+ */
+ private void autoSave() {
+ Log.d(LOG_TAG_AUTOSAVE, "STARTAUTOSAVE");
+ saveActive = true;
+ saveNote(new ISyncCallback() {
+ @Override
+ public void onFinish() {
+ onSaved();
+ }
+
+ @Override
+ public void onScheduled() {
+ onSaved();
+ }
+
+ private void onSaved() {
+ // AFTER SYNCHRONIZATION
+ Log.d(LOG_TAG_AUTOSAVE, "FINISHED AUTOSAVE");
+ saveActive = false;
+
+ // AFTER "DELAY_AFTER_SYNC" SECONDS: allow next auto-save or start it directly
+ handler.postDelayed(runAutoSave, DELAY_AFTER_SYNC);
+
+ }
+ });
+ }
+
+ @Override
+ protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) {
+ if (binding != null && isAttachedToWindow(binding.editContent)) {
+ binding.editContent.clearFocus();
+ binding.editContent.setSearchText(newText, current);
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ super.applyBrand(mainColor, textColor);
+ binding.editContent.setSearchColor(mainColor);
+ binding.editContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent));
+ }
+
+ public static BaseNoteFragment newInstance(long accountId, long noteId) {
+ final var fragment = new NoteEditFragment();
+ final var args = new Bundle();
+ args.putLong(PARAM_NOTE_ID, noteId);
+ args.putLong(PARAM_ACCOUNT_ID, accountId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public static BaseNoteFragment newInstanceWithNewNote(Note newNote) {
+ final var fragment = new NoteEditFragment();
+ final var args = new Bundle();
+ args.putSerializable(PARAM_NEWNOTE, newNote);
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..9d26f50064be677dc5bdb6cab9ae500c65907bab
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
@@ -0,0 +1,195 @@
+package it.niedermann.owncloud.notes.edit;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.text.Layout;
+import android.text.method.LinkMovementMethod;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ScrollView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.util.SSOUtil;
+
+import static androidx.core.view.ViewCompat.isAttachedToWindow;
+import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
+
+public class NotePreviewFragment extends SearchableBaseNoteFragment implements OnRefreshListener {
+
+ private static final String TAG = NotePreviewFragment.class.getSimpleName();
+
+ private String changedText;
+
+ protected FragmentNotePreviewBinding binding;
+
+ private boolean noteLoaded = false;
+
+ @Nullable
+ private Runnable setScrollY;
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.menu_edit).setVisible(true);
+ menu.findItem(R.id.menu_preview).setVisible(false);
+ }
+
+ @Override
+ public ScrollView getScrollView() {
+ return binding.scrollView;
+ }
+
+ @Override
+ protected synchronized void scrollToY(int y) {
+ this.setScrollY = () -> {
+ if (binding != null) {
+ Log.v("SCROLL set (preview) to", y + "");
+ binding.scrollView.post(() -> binding.scrollView.setScrollY(y));
+ }
+ setScrollY = null;
+ };
+ }
+
+ @Override
+ protected FloatingActionButton getSearchNextButton() {
+ return binding.searchNext;
+ }
+
+ @Override
+ protected FloatingActionButton getSearchPrevButton() {
+ return binding.searchPrev;
+ }
+
+ @Override
+ protected Layout getLayout() {
+ binding.singleNoteContent.onPreDraw();
+ return binding.singleNoteContent.getLayout();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup
+ container, @Nullable Bundle savedInstanceState) {
+ binding = FragmentNotePreviewBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ binding.swiperefreshlayout.setOnRefreshListener(this);
+ registerInternalNoteLinkHandler();
+ binding.singleNoteContent.setMovementMethod(LinkMovementMethod.getInstance());
+
+ final var sp = PreferenceManager.getDefaultSharedPreferences(requireActivity().getApplicationContext());
+ binding.singleNoteContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, getFontSizeFromPreferences(requireContext(), sp));
+ if (sp.getBoolean(getString(R.string.pref_key_font), false)) {
+ binding.singleNoteContent.setTypeface(Typeface.MONOSPACE);
+ }
+ }
+
+ @Override
+ protected void onNoteLoaded(Note note) {
+ super.onNoteLoaded(note);
+ noteLoaded = true;
+ registerInternalNoteLinkHandler();
+ changedText = note.getContent();
+ binding.singleNoteContent.setMarkdownString(note.getContent(), setScrollY);
+ binding.singleNoteContent.getMarkdownString().observe(requireActivity(), (newContent) -> {
+ changedText = newContent.toString();
+ saveNote(null);
+ });
+ }
+
+ protected void registerInternalNoteLinkHandler() {
+ binding.singleNoteContent.registerOnLinkClickCallback((link) -> {
+ try {
+ final long noteLocalId = repo.getLocalIdByRemoteId(this.note.getAccountId(), Long.parseLong(link));
+ Log.i(TAG, "Found note for remoteId \"" + link + "\" in account \"" + this.note.getAccountId() + "\" with localId + \"" + noteLocalId + "\". Attempt to open " + EditNoteActivity.class.getSimpleName() + " for this note.");
+ startActivity(new Intent(requireActivity().getApplicationContext(), EditNoteActivity.class).putExtra(EditNoteActivity.PARAM_NOTE_ID, noteLocalId));
+ return true;
+ } catch (NumberFormatException e) {
+ // Clicked link is not a long and therefore can't be a remote id.
+ } catch (IllegalArgumentException e) {
+ Log.i(TAG, "It looks like \"" + link + "\" might be a remote id of a note, but a note with this remote id could not be found in account \"" + note.getAccountId() + "\" .", e);
+ }
+ return false;
+ });
+ }
+
+ @Override
+ protected void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor) {
+ if (binding != null && isAttachedToWindow(binding.singleNoteContent)) {
+ binding.singleNoteContent.clearFocus();
+ binding.singleNoteContent.setSearchText(newText, current);
+ }
+ }
+
+ @Override
+ protected String getContent() {
+ return changedText;
+ }
+
+ @Override
+ public void onRefresh() {
+ if (noteLoaded && repo.isSyncPossible() && SSOUtil.isConfigured(getContext())) {
+ binding.swiperefreshlayout.setRefreshing(true);
+ executor.submit(() -> {
+ try {
+ final var account = repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()).name);
+ repo.addCallbackPull(account, () -> executor.submit(() -> {
+ note = repo.getNoteById(note.getId());
+ changedText = note.getContent();
+ requireActivity().runOnUiThread(() -> {
+ binding.singleNoteContent.setMarkdownString(note.getContent());
+ binding.swiperefreshlayout.setRefreshing(false);
+ });
+ }));
+ repo.scheduleSync(account, false);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ e.printStackTrace();
+ }
+ });
+ } else {
+ binding.swiperefreshlayout.setRefreshing(false);
+ Toast.makeText(requireContext(), getString(R.string.error_sync, getString(R.string.error_no_network)), Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ super.applyBrand(mainColor, textColor);
+ binding.singleNoteContent.setSearchColor(mainColor);
+ binding.singleNoteContent.setHighlightColor(getTextHighlightBackgroundColor(requireContext(), mainColor, colorPrimary, colorAccent));
+ }
+
+ public static BaseNoteFragment newInstance(long accountId, long noteId) {
+ final var fragment = new NotePreviewFragment();
+ final var args = new Bundle();
+ args.putLong(PARAM_NOTE_ID, noteId);
+ args.putLong(PARAM_ACCOUNT_ID, accountId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..55771c247909e060ce86cd15dd174f6130398a9a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java
@@ -0,0 +1,69 @@
+package it.niedermann.owncloud.notes.edit;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+
+public class NoteReadonlyFragment extends NotePreviewFragment {
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.findItem(R.id.menu_favorite).setVisible(false);
+ menu.findItem(R.id.menu_edit).setVisible(false);
+ menu.findItem(R.id.menu_preview).setVisible(false);
+ menu.findItem(R.id.menu_cancel).setVisible(false);
+ menu.findItem(R.id.menu_delete).setVisible(false);
+ menu.findItem(R.id.menu_share).setVisible(false);
+ menu.findItem(R.id.menu_move).setVisible(false);
+ menu.findItem(R.id.menu_category).setVisible(false);
+ menu.findItem(R.id.menu_title).setVisible(false);
+ if (menu.findItem(MENU_ID_PIN) != null)
+ menu.findItem(MENU_ID_PIN).setVisible(false);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ binding.singleNoteContent.setEnabled(false);
+ binding.swiperefreshlayout.setEnabled(false);
+ return binding.getRoot();
+ }
+
+ @Override
+ protected void registerInternalNoteLinkHandler() {
+ // Do nothing
+ }
+
+ @Override
+ public void showEditTitleDialog() {
+ // Do nothing
+ }
+
+ @Override
+ public void onCloseNote() {
+ // Do nothing
+ }
+
+ @Override
+ protected void saveNote(@Nullable ISyncCallback callback) {
+ // Do nothing
+ }
+
+ public static BaseNoteFragment newInstance(String content) {
+ final var fragment = new NoteReadonlyFragment();
+ final var args = new Bundle();
+ args.putString(PARAM_CONTENT, content);
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..794bfee71c82a68458400c14ab6b1f9684cac3e1
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/SearchableBaseNoteFragment.java
@@ -0,0 +1,301 @@
+package it.niedermann.owncloud.notes.edit;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.SearchView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedActivity;
+
+public abstract class SearchableBaseNoteFragment extends BaseNoteFragment {
+
+ private static final String TAG = SearchableBaseNoteFragment.class.getSimpleName();
+ private static final String saved_instance_key_searchQuery = "searchQuery";
+ private static final String saved_instance_key_currentOccurrence = "currentOccurrence";
+
+ private int currentOccurrence = 1;
+ private int occurrenceCount = 0;
+ private SearchView searchView;
+ private String searchQuery = null;
+ private static final int delay = 50; // If the search string does not change after $delay ms, then the search task starts.
+
+ @ColorInt
+ private int mainColor;
+ @ColorInt
+ private int textColor;
+
+ @Override
+ public void onStart() {
+ this.mainColor = getResources().getColor(R.color.defaultBrand);
+ this.textColor = Color.WHITE;
+ super.onStart();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ if (savedInstanceState != null) {
+ searchQuery = savedInstanceState.getString(saved_instance_key_searchQuery, "");
+ currentOccurrence = savedInstanceState.getInt(saved_instance_key_currentOccurrence, 1);
+ }
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ final var searchMenuItem = menu.findItem(R.id.search);
+ searchView = (SearchView) searchMenuItem.getActionView();
+
+ if (!TextUtils.isEmpty(searchQuery) && isNew) {
+ searchMenuItem.expandActionView();
+ searchView.setQuery(searchQuery, true);
+ searchView.clearFocus();
+ }
+
+ searchMenuItem.collapseActionView();
+
+ final var searchEditFrame = searchView.findViewById(R.id
+ .search_edit_frame);
+
+ searchEditFrame.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ int oldVisibility = -1;
+
+ @Override
+ public void onGlobalLayout() {
+ final int currentVisibility = searchEditFrame.getVisibility();
+
+ if (currentVisibility != oldVisibility) {
+ if (currentVisibility != View.VISIBLE) {
+ colorWithText("", null, mainColor, textColor);
+ searchQuery = "";
+ hideSearchFabs();
+ } else {
+ jumpToOccurrence();
+ colorWithText(searchQuery, null, mainColor, textColor);
+ occurrenceCount = countOccurrences(getContent(), searchQuery);
+ showSearchFabs();
+ }
+
+ oldVisibility = currentVisibility;
+ }
+ }
+
+ });
+
+ final var next = getSearchNextButton();
+ final var prev = getSearchPrevButton();
+
+ if (next != null) {
+ next.setOnClickListener(v -> {
+ currentOccurrence++;
+ jumpToOccurrence();
+ colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor);
+ });
+ }
+
+ if (prev != null) {
+ prev.setOnClickListener(v -> {
+ occurrenceCount = countOccurrences(getContent(), searchView.getQuery().toString());
+ currentOccurrence--;
+ jumpToOccurrence();
+ colorWithText(searchView.getQuery().toString(), currentOccurrence, mainColor, textColor);
+ });
+ }
+
+ searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ private DelayQueryRunnable delayQueryTask;
+ private final Handler handler = new Handler();
+
+ @Override
+ public boolean onQueryTextSubmit(@NonNull String query) {
+ currentOccurrence++;
+ jumpToOccurrence();
+ colorWithText(query, currentOccurrence, mainColor, textColor);
+ return true;
+ }
+
+ @Override
+ public boolean onQueryTextChange(@NonNull String newText) {
+ queryWithHandler(newText);
+ return true;
+ }
+
+ private void queryMatch(@NonNull String newText) {
+ searchQuery = newText;
+ occurrenceCount = countOccurrences(getContent(), searchQuery);
+ if (occurrenceCount > 1) {
+ showSearchFabs();
+ } else {
+ hideSearchFabs();
+ }
+ currentOccurrence = 1;
+ jumpToOccurrence();
+ colorWithText(searchQuery, currentOccurrence, mainColor, textColor);
+ }
+
+ private void queryWithHandler(@NonNull String newText) {
+ if (delayQueryTask != null) {
+ delayQueryTask.cancel();
+ handler.removeCallbacksAndMessages(null);
+ }
+ delayQueryTask = new DelayQueryRunnable(newText);
+ // If there is only one char in the search pattern, we should start the search immediately.
+ handler.postDelayed(delayQueryTask, newText.length() > 1 ? delay : 0);
+ }
+
+ class DelayQueryRunnable implements Runnable {
+ private String text;
+ private boolean canceled = false;
+
+ public DelayQueryRunnable(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public void run() {
+ if (canceled) {
+ return;
+ }
+ queryMatch(text);
+ }
+
+ public void cancel() {
+ canceled = true;
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (searchView != null && !TextUtils.isEmpty(searchView.getQuery().toString())) {
+ outState.putString(saved_instance_key_searchQuery, searchView.getQuery().toString());
+ outState.putInt(saved_instance_key_currentOccurrence, currentOccurrence);
+ }
+ }
+
+ protected abstract void colorWithText(@NonNull String newText, @Nullable Integer current, int mainColor, int textColor);
+
+ protected abstract Layout getLayout();
+
+ protected abstract FloatingActionButton getSearchNextButton();
+
+ protected abstract FloatingActionButton getSearchPrevButton();
+
+ private void showSearchFabs() {
+ final var next = getSearchNextButton();
+ final var prev = getSearchPrevButton();
+ if (prev != null) {
+ prev.show();
+ }
+ if (next != null) {
+ next.show();
+ }
+ }
+
+ private void hideSearchFabs() {
+ final var next = getSearchNextButton();
+ final var prev = getSearchPrevButton();
+ if (prev != null) {
+ prev.hide();
+ }
+ if (next != null) {
+ next.hide();
+ }
+ }
+
+ private void jumpToOccurrence() {
+ final var layout = getLayout();
+ if (layout == null) {
+ Log.w(TAG, "getLayout() is null");
+ } else if (getContent() == null || getContent().isEmpty()) {
+ Log.w(TAG, "getContent is null or empty");
+ } else if (currentOccurrence < 1) {
+ // if currentOccurrence is lower than 1, jump to last occurrence
+ currentOccurrence = occurrenceCount;
+ jumpToOccurrence();
+ } else if (searchQuery != null && !searchQuery.isEmpty()) {
+ final String currentContent = getContent().toLowerCase();
+ final int indexOfNewText = indexOfNth(currentContent, searchQuery.toLowerCase(), 0, currentOccurrence);
+ if (indexOfNewText <= 0) {
+ // Search term is not n times in text
+ // Go back to first search result
+ if (currentOccurrence != 1) {
+ currentOccurrence = 1;
+ jumpToOccurrence();
+ }
+ return;
+ }
+ final String textUntilFirstOccurrence = currentContent.substring(0, indexOfNewText);
+ final int numberLine = layout.getLineForOffset(textUntilFirstOccurrence.length());
+
+ if (numberLine >= 0) {
+ final var scrollView = getScrollView();
+ if (scrollView != null) {
+ scrollView.post(() -> scrollView.smoothScrollTo(0, layout.getLineTop(numberLine)));
+ }
+ }
+ }
+ }
+
+ private static int indexOfNth(String input, String value, int startIndex, int nth) {
+ if (nth < 1)
+ throw new IllegalArgumentException("Param 'nth' must be greater than 0!");
+ if (nth == 1)
+ return input.indexOf(value, startIndex);
+ final int idx = input.indexOf(value, startIndex);
+ if (idx == -1)
+ return -1;
+ return indexOfNth(input, value, idx + 1, nth - 1);
+ }
+
+ private static int countOccurrences(String haystack, String needle) {
+ if (haystack == null || haystack.isEmpty() || needle == null || needle.isEmpty()) {
+ return 0;
+ }
+ // Use regrex which is faster before.
+ // Such that the main thread will not stop for a long tilme
+ // And so there will not an ANR problem
+ final var matcher = Pattern.compile(needle, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
+ .matcher(haystack);
+
+ int count = 0;
+ while (matcher.find()) {
+ count++;
+ }
+ return count;
+ }
+
+ @CallSuper
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ this.mainColor = mainColor;
+ this.textColor = textColor;
+ BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchPrevButton());
+ BrandedActivity.applyBrandToFAB(mainColor, textColor, getSearchNextButton());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..cae09cc02c5cd26c284db9cd1daa9bc44a3dd7ab
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryAdapter.java
@@ -0,0 +1,134 @@
+package it.niedermann.owncloud.notes.edit.category;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemCategoryBinding;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.shared.util.NoteUtil;
+
+public class CategoryAdapter extends RecyclerView.Adapter {
+
+ private static final String clearItemId = "clear_item";
+ private static final String addItemId = "add_item";
+ @NonNull
+ private final List categories = new ArrayList<>();
+ @NonNull
+ private final CategoryListener listener;
+ private final Context context;
+
+ CategoryAdapter(@NonNull Context context, @NonNull CategoryListener categoryListener) {
+ this.context = context;
+ this.listener = categoryListener;
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final var view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_category, parent, false);
+ return new CategoryViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ final var category = categories.get(position);
+ final var categoryViewHolder = (CategoryViewHolder) holder;
+
+ switch (category.id) {
+ case addItemId:
+ final var wrapDrawable = DrawableCompat.wrap(ContextCompat.getDrawable(context, category.icon));
+ DrawableCompat.setTint(wrapDrawable, ContextCompat.getColor(context, R.color.icon_color_default));
+ categoryViewHolder.getIcon().setImageDrawable(wrapDrawable);
+ categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryAdded());
+ break;
+ case clearItemId:
+ categoryViewHolder.getIcon().setImageDrawable(ContextCompat.getDrawable(context, category.icon));
+ categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryCleared());
+ break;
+ default:
+ categoryViewHolder.getIcon().setImageDrawable(ContextCompat.getDrawable(context, category.icon));
+ categoryViewHolder.getCategoryWrapper().setOnClickListener((v) -> listener.onCategoryChosen(category.label));
+ break;
+ }
+ categoryViewHolder.getCategory().setText(NoteUtil.extendCategory(category.label));
+ if (category.count != null && category.count > 0) {
+ categoryViewHolder.getCount().setText(String.valueOf(category.count));
+ } else {
+ categoryViewHolder.getCount().setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return categories.size();
+ }
+
+ static class CategoryViewHolder extends RecyclerView.ViewHolder {
+ private final ItemCategoryBinding binding;
+
+ private CategoryViewHolder(View view) {
+ super(view);
+ binding = ItemCategoryBinding.bind(view);
+ }
+
+ private View getCategoryWrapper() {
+ return binding.categoryWrapper;
+ }
+
+ private AppCompatImageView getIcon() {
+ return binding.icon;
+ }
+
+ private TextView getCategory() {
+ return binding.category;
+ }
+
+ private TextView getCount() {
+ return binding.count;
+ }
+ }
+
+ void setCategoryList(List categories, @Nullable String currentSearchString) {
+ this.categories.clear();
+ this.categories.addAll(categories);
+ final NavigationItem clearItem = new NavigationItem(clearItemId, context.getString(R.string.no_category), 0, R.drawable.ic_clear_grey_24dp);
+ this.categories.add(0, clearItem);
+ if (currentSearchString != null && currentSearchString.trim().length() > 0) {
+ boolean currentSearchStringIsInCategories = false;
+ for (final var category : categories) {
+ if (currentSearchString.equals(category.label)) {
+ currentSearchStringIsInCategories = true;
+ break;
+ }
+ }
+ if (!currentSearchStringIsInCategories) {
+ final var addItem = new NavigationItem(addItemId, context.getString(R.string.add_category, currentSearchString.trim()), 0, R.drawable.ic_add_blue_24dp);
+ this.categories.add(addItem);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public interface CategoryListener {
+ void onCategoryChosen(String category);
+
+ void onCategoryAdded();
+
+ void onCategoryCleared();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..530a55497ca2c8e68662dc1f540606d04557d19c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryDialogFragment.java
@@ -0,0 +1,197 @@
+package it.niedermann.owncloud.notes.edit.category;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedAlertDialogBuilder;
+import it.niedermann.owncloud.notes.branding.BrandedDialogFragment;
+import it.niedermann.owncloud.notes.branding.BrandingUtil;
+import it.niedermann.owncloud.notes.databinding.DialogChangeCategoryBinding;
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+
+/**
+ * This {@link DialogFragment} allows for the selection of a category.
+ * It targetFragment is set it must implement the interface {@link CategoryDialogListener}.
+ * The calling Activity must implement the interface {@link CategoryDialogListener}.
+ */
+public class CategoryDialogFragment extends BrandedDialogFragment {
+
+ private static final String TAG = CategoryDialogFragment.class.getSimpleName();
+ private static final String STATE_CATEGORY = "category";
+
+ private CategoryViewModel viewModel;
+ private DialogChangeCategoryBinding binding;
+
+ private CategoryDialogListener listener;
+
+ private CategoryAdapter adapter;
+
+ private EditText editCategory;
+
+ private LiveData> categoryLiveData;
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ BrandingUtil.applyBrandToEditText(mainColor, textColor, binding.search);
+ }
+
+ /**
+ * Interface that must be implemented by the calling Activity.
+ */
+ public interface CategoryDialogListener {
+ /**
+ * This method is called after the user has chosen a category.
+ *
+ * @param category Name of the category which was chosen by the user.
+ */
+ void onCategoryChosen(String category);
+ }
+
+ public static final String PARAM_ACCOUNT_ID = "account_id";
+ public static final String PARAM_CATEGORY = "category";
+
+ private long accountId;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getArguments() != null && requireArguments().containsKey(PARAM_ACCOUNT_ID)) {
+ accountId = requireArguments().getLong(PARAM_ACCOUNT_ID);
+ } else {
+ throw new IllegalArgumentException("Provide at least \"" + PARAM_ACCOUNT_ID + "\"");
+ }
+ final var target = getTargetFragment();
+ if (target instanceof CategoryDialogListener) {
+ listener = (CategoryDialogListener) target;
+ } else if (getActivity() instanceof CategoryDialogListener) {
+ listener = (CategoryDialogListener) getActivity();
+ } else {
+ throw new IllegalArgumentException("Calling activity or target fragment must implement " + CategoryDialogListener.class.getSimpleName());
+ }
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.viewModel = new ViewModelProvider(requireActivity()).get(CategoryViewModel.class);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final var dialogView = View.inflate(getContext(), R.layout.dialog_change_category, null);
+ binding = DialogChangeCategoryBinding.bind(dialogView);
+ this.editCategory = binding.search;
+
+ if (savedInstanceState == null) {
+ if (requireArguments().containsKey(PARAM_CATEGORY)) {
+ editCategory.setText(requireArguments().getString(PARAM_CATEGORY));
+ }
+ } else if (savedInstanceState.containsKey(STATE_CATEGORY)) {
+ editCategory.setText(savedInstanceState.getString(STATE_CATEGORY));
+ }
+
+ adapter = new CategoryAdapter(requireContext(), new CategoryAdapter.CategoryListener() {
+ @Override
+ public void onCategoryChosen(String category) {
+ listener.onCategoryChosen(category);
+ dismiss();
+ }
+
+ @Override
+ public void onCategoryAdded() {
+ listener.onCategoryChosen(editCategory.getText().toString());
+ dismiss();
+ }
+
+ @Override
+ public void onCategoryCleared() {
+ listener.onCategoryChosen("");
+ dismiss();
+ }
+ });
+
+ binding.recyclerView.setAdapter(adapter);
+
+ categoryLiveData = viewModel.getCategories(accountId);
+ categoryLiveData.observe(requireActivity(), categories -> adapter.setCategoryList(categories, binding.search.getText().toString()));
+
+ editCategory.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Nothing to do here...
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Nothing to do here...
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ viewModel.postSearchTerm(editCategory.getText().toString());
+ }
+ });
+
+ return new BrandedAlertDialogBuilder(getActivity())
+ .setTitle(R.string.change_category_title)
+ .setView(dialogView)
+ .setCancelable(true)
+ .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onCategoryChosen(editCategory.getText().toString()))
+ .setNegativeButton(R.string.simple_cancel, null)
+ .create();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(STATE_CATEGORY, editCategory.getText().toString());
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (editCategory.getText() == null || editCategory.getText().length() == 0) {
+ editCategory.requestFocus();
+ if (getDialog() != null && getDialog().getWindow() != null) {
+ getDialog().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ } else {
+ Log.w(TAG, "can not set SOFT_INPUT_STATE_ALWAYAS_VISIBLE because getWindow() == null");
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (categoryLiveData != null) {
+ categoryLiveData.removeObservers(requireActivity());
+ }
+ }
+
+ public static DialogFragment newInstance(long accountId, String category) {
+ final var categoryFragment = new CategoryDialogFragment();
+ final var args = new Bundle();
+ args.putString(CategoryDialogFragment.PARAM_CATEGORY, category);
+ args.putLong(CategoryDialogFragment.PARAM_ACCOUNT_ID, accountId);
+ categoryFragment.setArguments(args);
+ return categoryFragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea5efd37aae1da727b9c46761d8556e606239ddc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/category/CategoryViewModel.java
@@ -0,0 +1,42 @@
+package it.niedermann.owncloud.notes.edit.category;
+
+import android.app.Application;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import java.util.List;
+
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem;
+
+public class CategoryViewModel extends AndroidViewModel {
+
+ private final NotesRepository repo;
+
+ @NonNull
+ private final MutableLiveData searchTerm = new MutableLiveData<>("");
+
+ public CategoryViewModel(@NonNull Application application) {
+ super(application);
+ repo = NotesRepository.getInstance(application);
+ }
+
+ public void postSearchTerm(@NonNull String searchTerm) {
+ this.searchTerm.postValue(searchTerm);
+ }
+
+ @NonNull
+ public LiveData> getCategories(long accountId) {
+ return switchMap(this.searchTerm, searchTerm ->
+ map(repo.searchCategories$(accountId, TextUtils.isEmpty(searchTerm) ? "%" : "%" + searchTerm + "%"),
+ categories -> convertToCategoryNavigationItem(getApplication(), categories)));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..d372cdcb7b33b73ad0c37d7e5bf3e66642dab8e5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/title/EditTitleDialogFragment.java
@@ -0,0 +1,96 @@
+package it.niedermann.owncloud.notes.edit.title;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.DialogEditTitleBinding;
+
+public class EditTitleDialogFragment extends DialogFragment {
+
+ private static final String TAG = EditTitleDialogFragment.class.getSimpleName();
+ static final String PARAM_OLD_TITLE = "old_title";
+ private DialogEditTitleBinding binding;
+
+ private String oldTitle;
+ private EditTitleListener listener;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ final var args = getArguments();
+ if (args == null) {
+ throw new IllegalArgumentException("Provide at least " + PARAM_OLD_TITLE);
+ }
+ oldTitle = args.getString(PARAM_OLD_TITLE);
+
+ if (getTargetFragment() instanceof EditTitleListener) {
+ listener = (EditTitleListener) getTargetFragment();
+ } else if (getActivity() instanceof EditTitleListener) {
+ listener = (EditTitleListener) getActivity();
+ } else {
+ throw new IllegalArgumentException("Calling activity or target fragment must implement " + EditTitleListener.class.getSimpleName());
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final var dialogView = View.inflate(getContext(), R.layout.dialog_edit_title, null);
+ binding = DialogEditTitleBinding.bind(dialogView);
+
+ if (savedInstanceState == null) {
+ binding.title.setText(oldTitle);
+ }
+
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.change_note_title)
+ .setView(dialogView)
+ .setCancelable(true)
+ .setPositiveButton(R.string.action_edit_save, (dialog, which) -> listener.onTitleEdited(binding.title.getText().toString()))
+ .setNegativeButton(R.string.simple_cancel, null)
+ .create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ binding.title.requestFocus();
+ final var window = requireDialog().getWindow();
+ if (window != null) {
+ window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ } else {
+ Log.w(TAG, "can not enable soft keyboard because " + Window.class.getSimpleName() + " is null.");
+ }
+ }
+
+ public static DialogFragment newInstance(String title) {
+ final var fragment = new EditTitleDialogFragment();
+ final var args = new Bundle();
+ args.putString(PARAM_OLD_TITLE, title);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ /**
+ * Interface that must be implemented by the calling Activity.
+ */
+ public interface EditTitleListener {
+ /**
+ * This method is called after the user has changed the title of a note manually.
+ *
+ * @param newTitle the new title that a user submitted
+ */
+ void onTitleEdited(String newTitle);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..9664dfaf153255624f54a4ef1073a7e2d13fe7e5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionActivity.java
@@ -0,0 +1,62 @@
+package it.niedermann.owncloud.notes.exception;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import java.util.Collections;
+
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
+import it.niedermann.owncloud.notes.BuildConfig;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ActivityExceptionBinding;
+import it.niedermann.owncloud.notes.exception.tips.TipsAdapter;
+
+
+public class ExceptionActivity extends AppCompatActivity {
+
+ private static final String KEY_THROWABLE = "throwable";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final var binding = ActivityExceptionBinding.inflate(getLayoutInflater());
+
+ setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+
+ var throwable = ((Throwable) getIntent().getSerializableExtra(KEY_THROWABLE));
+
+ if (throwable == null) {
+ throwable = new Exception("Could not get exception");
+ }
+
+ final var adapter = new TipsAdapter(this::startActivity);
+ final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(this, throwable);
+
+ binding.tips.setAdapter(adapter);
+ binding.tips.setNestedScrollingEnabled(false);
+ binding.toolbar.setTitle(getString(R.string.simple_error));
+ binding.message.setText(throwable.getMessage());
+ binding.stacktrace.setText(debugInfos);
+ binding.copy.setOnClickListener((v) -> ClipboardUtil.INSTANCE.copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"));
+ binding.close.setOnClickListener((v) -> finish());
+
+ adapter.setThrowables(Collections.singletonList(throwable));
+ }
+
+ @NonNull
+ public static Intent createIntent(@NonNull Context context, Throwable throwable) {
+ final var args = new Bundle();
+ args.putSerializable(KEY_THROWABLE, throwable);
+ return new Intent(context, ExceptionActivity.class)
+ .putExtras(args)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..b38f3495e35b3b752f20ccd8c6a9515234015a56
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionDialogFragment.java
@@ -0,0 +1,90 @@
+package it.niedermann.owncloud.notes.exception;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.fragment.app.DialogFragment;
+
+import java.util.ArrayList;
+
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
+import it.niedermann.owncloud.notes.BuildConfig;
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.DialogExceptionBinding;
+import it.niedermann.owncloud.notes.exception.tips.TipsAdapter;
+
+public class ExceptionDialogFragment extends AppCompatDialogFragment {
+
+ private static final String KEY_THROWABLES = "throwables";
+ public static final String INTENT_EXTRA_BUTTON_TEXT = "button_text";
+
+ @NonNull
+ private final ArrayList throwables = new ArrayList<>();
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ final var args = getArguments();
+ if (args != null) {
+ final var throwablesArgument = args.getSerializable(KEY_THROWABLES);
+ if (throwablesArgument instanceof Iterable>) {
+ for (final var arg : (Iterable>) throwablesArgument) {
+ if (arg instanceof Throwable) {
+ throwables.add((Throwable) arg);
+ } else {
+ throw new IllegalArgumentException("Expected all " + KEY_THROWABLES + " to be instance of " + Throwable.class.getSimpleName());
+ }
+ }
+ } else {
+ throw new IllegalArgumentException(KEY_THROWABLES + " needs to be an " + Iterable.class.getSimpleName() + "<" + Throwable.class.getSimpleName() + ">");
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final var view = View.inflate(getContext(), R.layout.dialog_exception, null);
+ final var binding = DialogExceptionBinding.bind(view);
+
+ final var adapter = new TipsAdapter((actionIntent) -> requireActivity().startActivity(actionIntent));
+
+ final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), throwables);
+
+ binding.tips.setAdapter(adapter);
+ binding.stacktrace.setText(debugInfos);
+
+ adapter.setThrowables(throwables);
+
+ return new AlertDialog.Builder(requireActivity())
+ .setView(binding.getRoot())
+ .setTitle(R.string.error_dialog_title)
+ .setPositiveButton(android.R.string.copy, (a, b) -> ClipboardUtil.INSTANCE.copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"))
+ .setNegativeButton(R.string.simple_close, null)
+ .create();
+ }
+
+ public static DialogFragment newInstance(ArrayList exceptions) {
+ final var args = new Bundle();
+ args.putSerializable(KEY_THROWABLES, exceptions);
+ final var fragment = new ExceptionDialogFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public static DialogFragment newInstance(Throwable exception) {
+ final var args = new Bundle();
+ final var list = new ArrayList(1);
+ list.add(exception);
+ args.putSerializable(KEY_THROWABLES, list);
+ final var fragment = new ExceptionDialogFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e90bd72469da88e05c0452b3b4ed140e9e0d522
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/ExceptionHandler.java
@@ -0,0 +1,27 @@
+package it.niedermann.owncloud.notes.exception;
+
+import android.app.Activity;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+
+public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ private static final String TAG = ExceptionHandler.class.getSimpleName();
+
+ @NonNull
+ private final Activity activity;
+
+ public ExceptionHandler(@NonNull Activity activity) {
+ this.activity = activity;
+ }
+
+ @Override
+ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
+ Log.e(TAG, e.getMessage(), e);
+ activity.getApplicationContext().startActivity(ExceptionActivity.createIntent(activity.getApplicationContext(), e));
+ activity.finish();
+ Runtime.getRuntime().exit(0);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cd1f206fbbb43b64bd30c9f6733665cc27b06f5
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/IntendedOfflineException.java
@@ -0,0 +1,14 @@
+package it.niedermann.owncloud.notes.exception;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This type of {@link Exception} occurs, when a user has an active internet connection but decided by intention not to use it.
+ * Example: "Sync only on Wi-Fi" is set to true
, Wi-Fi is not connected, mobile data is available
+ */
+public class IntendedOfflineException extends Exception {
+
+ public IntendedOfflineException(@NonNull String message) {
+ super(message);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f82b612dbbad966b6a20e35567426511a562143c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsAdapter.java
@@ -0,0 +1,126 @@
+package it.niedermann.owncloud.notes.exception.tips;
+
+import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
+import static it.niedermann.owncloud.notes.exception.ExceptionDialogFragment.INTENT_EXTRA_BUTTON_TEXT;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotSupportedException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.TokenMismatchException;
+import com.nextcloud.android.sso.exceptions.UnknownErrorException;
+
+import org.json.JSONException;
+
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.util.LinkedList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.BuildConfig;
+import it.niedermann.owncloud.notes.R;
+
+public class TipsAdapter extends RecyclerView.Adapter {
+
+ @NonNull
+ private final Consumer actionButtonClickedListener;
+ @NonNull
+ private final List tips = new LinkedList<>();
+
+ public TipsAdapter(@NonNull Consumer actionButtonClickedListener) {
+ this.actionButtonClickedListener = actionButtonClickedListener;
+ }
+
+ @NonNull
+ @Override
+ public TipsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final var view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_tip, parent, false);
+ return new TipsViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull TipsViewHolder holder, int position) {
+ holder.bind(tips.get(position), actionButtonClickedListener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return tips.size();
+ }
+
+ public void setThrowables(@NonNull List throwables) {
+ for (final var throwable : throwables) {
+ if (throwable instanceof TokenMismatchException) {
+ add(R.string.error_dialog_tip_token_mismatch_retry);
+ add(R.string.error_dialog_tip_token_mismatch_clear_storage);
+ final var intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
+ .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
+ add(R.string.error_dialog_tip_clear_storage, intent);
+ } else if (throwable instanceof NextcloudFilesAppNotSupportedException) {
+ add(R.string.error_dialog_tip_files_outdated);
+ } else if (throwable instanceof NextcloudApiNotRespondingException) {
+ if (VERSION.SDK_INT >= VERSION_CODES.M) {
+ add(R.string.error_dialog_tip_disable_battery_optimizations, new Intent().setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS).putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_battery_settings));
+ } else {
+ add(R.string.error_dialog_tip_disable_battery_optimizations);
+ }
+ add(R.string.error_dialog_tip_files_force_stop);
+ add(R.string.error_dialog_tip_files_delete_storage);
+ final var intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
+ .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
+ add(R.string.error_dialog_tip_clear_storage, intent);
+ } else if (throwable instanceof SocketTimeoutException || throwable instanceof ConnectException) {
+ add(R.string.error_dialog_timeout_instance);
+ add(R.string.error_dialog_timeout_toggle, new Intent(Settings.ACTION_WIFI_SETTINGS).putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_network));
+ } else if (throwable instanceof JSONException || throwable instanceof NullPointerException) {
+ add(R.string.error_dialog_check_server);
+ } else if (throwable instanceof NextcloudHttpRequestFailedException) {
+ final int statusCode = ((NextcloudHttpRequestFailedException) throwable).getStatusCode();
+ switch (statusCode) {
+ case 302:
+ add(R.string.error_dialog_server_app_enabled);
+ add(R.string.error_dialog_redirect);
+ break;
+ case 500:
+ add(R.string.error_dialog_check_server_logs);
+ break;
+ case 503:
+ add(R.string.error_dialog_check_maintenance);
+ break;
+ case 507:
+ add(R.string.error_dialog_insufficient_storage);
+ break;
+ }
+ } else if (throwable instanceof UnknownErrorException) {
+ if ("com.nextcloud.android.sso.QueryParam".equals(throwable.getMessage())) {
+ add(R.string.error_dialog_min_version, new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.nextcloud.client"))
+ .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_update_files_app));
+ }
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ private void add(@StringRes int text) {
+ add(text, null);
+ }
+
+ private void add(@StringRes int text, @Nullable Intent primaryAction) {
+ tips.add(new TipsModel(text, primaryAction));
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsModel.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..1197898da380adeabb49aaea2010be79205cf51d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsModel.java
@@ -0,0 +1,29 @@
+package it.niedermann.owncloud.notes.exception.tips;
+
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+@SuppressWarnings("WeakerAccess")
+public class TipsModel {
+ @StringRes
+ private final int text;
+ @Nullable
+ private final Intent actionIntent;
+
+ TipsModel(@StringRes int text, @Nullable Intent actionIntent) {
+ this.text = text;
+ this.actionIntent = actionIntent;
+ }
+
+ @StringRes
+ public int getText() {
+ return this.text;
+ }
+
+ @Nullable
+ public Intent getActionIntent() {
+ return this.actionIntent;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0c2a410df4d3f405ea220e4d18e4d6d39ab09ef
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/exception/tips/TipsViewHolder.java
@@ -0,0 +1,35 @@
+package it.niedermann.owncloud.notes.exception.tips;
+
+import android.content.Intent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.databinding.ItemTipBinding;
+
+import static it.niedermann.owncloud.notes.exception.ExceptionDialogFragment.INTENT_EXTRA_BUTTON_TEXT;
+
+
+public class TipsViewHolder extends RecyclerView.ViewHolder {
+ private final ItemTipBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public TipsViewHolder(@NonNull View itemView) {
+ super(itemView);
+ binding = ItemTipBinding.bind(itemView);
+ }
+
+ public void bind(TipsModel tip, Consumer actionButtonClickedListener) {
+ binding.tip.setText(tip.getText());
+ final var intent = tip.getActionIntent();
+ if (intent != null && intent.hasExtra(INTENT_EXTRA_BUTTON_TEXT)) {
+ binding.actionButton.setVisibility(View.VISIBLE);
+ binding.actionButton.setText(intent.getIntExtra(INTENT_EXTRA_BUTTON_TEXT, 0));
+ binding.actionButton.setOnClickListener((v) -> actionButtonClickedListener.accept(intent));
+ } else {
+ binding.actionButton.setVisibility(View.GONE);
+ }
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..54e92baf9ab125cb419a53fe499efa3e146c7325
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountActivity.java
@@ -0,0 +1,178 @@
+package it.niedermann.owncloud.notes.importaccount;
+
+import android.accounts.NetworkErrorException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+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;
+import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.UnknownErrorException;
+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.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;
+
+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;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityImportAccountBinding.inflate(getLayoutInflater());
+ importAccountViewModel = new ViewModelProvider(this).get(ImportAccountViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ binding.welcomeText.setText(getString(R.string.welcome_text, getString(R.string.app_name)));
+ binding.addButton.setOnClickListener((v) -> {
+ binding.addButton.setEnabled(false);
+ binding.status.setVisibility(View.GONE);
+ try {
+ AccountImporter.pickNewAccount(this);
+ } catch (NextcloudFilesAppNotInstalledException e) {
+ UiExceptionManager.showDialogForException(this, e);
+ Log.w(TAG, "=============================================================");
+ Log.w(TAG, "Nextcloud app is not installed. Cannot choose account");
+ e.printStackTrace();
+ } catch (AndroidGetAccountsPermissionNotGranted e) {
+ binding.addButton.setEnabled(true);
+ AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
+ }
+ });
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ super.onSupportNavigateUp();
+ setResult(RESULT_CANCELED);
+ return true;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ try {
+ AccountImporter.onActivityResult(requestCode, resultCode, data, ImportAccountActivity.this, ssoAccount -> {
+ runOnUiThread(() -> binding.progressCircular.setVisibility(View.VISIBLE));
+
+ SingleAccountHelper.setCurrentAccount(getApplicationContext(), ssoAccount.name);
+ 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 var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance());
+ final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance());
+ final var status$ = importAccountViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback<>() {
+
+ /**
+ * 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(() -> {
+ Log.i(TAG, capabilities.toString());
+ BrandingUtil.saveBrandColors(ImportAccountActivity.this, capabilities.getColor(), capabilities.getTextColor());
+ setResult(RESULT_OK);
+ finish();
+ });
+ SyncWorker.update(ImportAccountActivity.this, PreferenceManager.getDefaultSharedPreferences(ImportAccountActivity.this)
+ .getBoolean(getString(R.string.pref_key_background_sync), true));
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ binding.addButton.setEnabled(true);
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
+ }
+ });
+ runOnUiThread(() -> status$.observe(ImportAccountActivity.this, (status) -> {
+ binding.progressText.setVisibility(View.VISIBLE);
+ Log.v(TAG, "Status: " + status.count + " of " + status.total);
+ if(status.count > 0) {
+ binding.progressCircular.setIndeterminate(false);
+ }
+ binding.progressText.setText(getString(R.string.progress_import, status.count + 1, status.total));
+ binding.progressCircular.setProgress(status.count);
+ binding.progressCircular.setMax(status.total);
+ }));
+ } catch (Throwable t) {
+ t.printStackTrace();
+ ApiProvider.getInstance().invalidateAPICache(ssoAccount);
+ SingleAccountHelper.setCurrentAccount(this, null);
+ runOnUiThread(() -> {
+ restoreCleanState();
+ if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ binding.status.setText(R.string.error_maintenance_mode);
+ binding.status.setVisibility(View.VISIBLE);
+ } 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() != 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);
+ } else {
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+ });
+ });
+ } catch (AccountImportCancelledException e) {
+ restoreCleanState();
+ Log.i(TAG, "Account import has been canceled.");
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
+ }
+
+ private void restoreCleanState() {
+ runOnUiThread(() -> {
+ binding.addButton.setEnabled(true);
+ binding.progressCircular.setVisibility(View.GONE);
+ binding.progressText.setVisibility(View.GONE);
+ });
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..1d60a0434ca61c42b2ce1881b7b078139f8451e8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/importaccount/ImportAccountViewModel.java
@@ -0,0 +1,29 @@
+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;
+
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.ImportStatus;
+
+public class ImportAccountViewModel extends AndroidViewModel {
+
+ @NonNull
+ private final NotesRepository repo;
+
+ public ImportAccountViewModel(@NonNull Application application) {
+ super(application);
+ this.repo = NotesRepository.getInstance(application);
+ }
+
+ public LiveData addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback callback) {
+ return 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
new file mode 100644
index 0000000000000000000000000000000000000000..3eb32044d1eb570be1d3c0a496109916eaddfbe8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
@@ -0,0 +1,806 @@
+package it.niedermann.owncloud.notes.main;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.O;
+import static android.view.View.GONE;
+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.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;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
+import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
+import static it.niedermann.owncloud.notes.shared.util.SSOUtil.askForNewAccount;
+
+import android.accounts.NetworkErrorException;
+import android.animation.AnimatorInflater;
+import android.app.SearchManager;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBarDrawerToggle;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.view.ActionMode;
+import androidx.appcompat.widget.SearchView;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.view.GravityCompat;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.selection.SelectionTracker;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.StaggeredGridLayoutManager;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.exceptions.AccountImportCancelledException;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.exceptions.TokenMismatchException;
+import com.nextcloud.android.sso.exceptions.UnknownErrorException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.net.HttpURLConnection;
+import java.util.LinkedList;
+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;
+import it.niedermann.owncloud.notes.accountpicker.AccountPickerListener;
+import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherDialog;
+import it.niedermann.owncloud.notes.accountswitcher.AccountSwitcherListener;
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
+import it.niedermann.owncloud.notes.databinding.ActivityNotesListViewBinding;
+import it.niedermann.owncloud.notes.databinding.DrawerLayoutBinding;
+import it.niedermann.owncloud.notes.edit.EditNoteActivity;
+import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
+import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
+import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
+import it.niedermann.owncloud.notes.exception.IntendedOfflineException;
+import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity;
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration;
+import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper;
+import it.niedermann.owncloud.notes.main.items.section.SectionItemDecoration;
+import it.niedermann.owncloud.notes.main.items.selection.ItemSelectionTracker;
+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.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+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;
+
+public class MainActivity extends LockedActivity implements NoteClickListener, AccountPickerListener, AccountSwitcherListener, CategoryDialogFragment.CategoryDialogListener {
+
+ 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 ADAPTER_KEY_RECENT = "recent";
+ public static final String ADAPTER_KEY_STARRED = "starred";
+ public static final String ADAPTER_KEY_UNCATEGORIZED = "uncategorized";
+
+ private static final int REQUEST_CODE_CREATE_NOTE = 0;
+ private static final int REQUEST_CODE_SERVER_SETTINGS = 1;
+
+ protected ItemAdapter adapter;
+ private NavigationAdapter adapterCategories;
+ private MenuAdapter menuAdapter;
+
+ private SelectionTracker tracker;
+ private NotesListViewItemTouchHelper itemTouchHelper;
+
+ protected DrawerLayoutBinding binding;
+ protected ActivityNotesListViewBinding activityBinding;
+ protected FloatingActionButton fabCreate;
+ private CoordinatorLayout coordinatorLayout;
+ private SwipeRefreshLayout swipeRefreshLayout;
+ private RecyclerView listView;
+ private ActionMode mActionMode;
+
+ boolean canMoveNoteToAnotherAccounts = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ categoryViewModel = new ViewModelProvider(this).get(CategoryViewModel.class);
+ CapabilitiesWorker.update(this);
+ binding = DrawerLayoutBinding.inflate(getLayoutInflater());
+ activityBinding = ActivityNotesListViewBinding.bind(binding.activityNotesListView.getRoot());
+
+ setContentView(binding.getRoot());
+
+ this.coordinatorLayout = binding.activityNotesListView.activityNotesListView;
+ this.swipeRefreshLayout = binding.activityNotesListView.swiperefreshlayout;
+ this.fabCreate = binding.activityNotesListView.fabCreate;
+ this.listView = binding.activityNotesListView.recyclerView;
+
+ gridView = isGridViewEnabled();
+
+ if (!gridView || isDarkThemeActive(this)) {
+ activityBinding.activityNotesListView.setBackgroundColor(ContextCompat.getColor(this, R.color.primary));
+ }
+
+ setupToolbars();
+ setupNavigationList();
+ setupNotesList();
+
+ mainViewModel.getAccountsCount().observe(this, (count) -> {
+ if (count == 0) {
+ startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ } else {
+ executor.submit(() -> {
+ try {
+ final var account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
+ runOnUiThread(() -> mainViewModel.postCurrentAccount(account));
+ } 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 var modifiedNotes = new LinkedList();
+ for (final var account : mainViewModel.getAccounts()) {
+ modifiedNotes.addAll(mainViewModel.getLocalModifiedNotes(account.getId()));
+ }
+ if (modifiedNotes.size() == 1) {
+ final var 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 var ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext());
+ final var 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 (final var 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()));
+ }
+ });
+ }
+ });
+
+ mainViewModel.hasMultipleAccountsConfigured().observe(this, hasMultipleAccountsConfigured -> canMoveNoteToAnotherAccounts = hasMultipleAccountsConfigured);
+ mainViewModel.getSyncStatus().observe(this, syncStatus -> swipeRefreshLayout.setRefreshing(syncStatus));
+ mainViewModel.getSyncErrors().observe(this, exceptions -> {
+ if (mainViewModel.containsNonInfrastructureRelatedItems(exceptions)) {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(exceptions)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
+ }
+ });
+ mainViewModel.getSelectedCategory().observe(this, (selectedCategory) -> {
+ binding.activityNotesListView.emptyContentView.getRoot().setVisibility(GONE);
+ adapter.setShowCategory(selectedCategory.getType() == RECENT || selectedCategory.getType() == FAVORITES);
+ fabCreate.show();
+
+ switch (selectedCategory.getType()) {
+ case RECENT: {
+ activityBinding.searchText.setText(getString(R.string.search_in_all));
+ break;
+ }
+ case FAVORITES: {
+ activityBinding.searchText.setText(getString(R.string.search_in_category, getString(R.string.label_favorites)));
+ break;
+ }
+ case UNCATEGORIZED: {
+ activityBinding.searchText.setText(getString(R.string.search_in_category, getString(R.string.action_uncategorized)));
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category == null) {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ activityBinding.searchText.setText(getString(R.string.search_in_category, NoteUtil.extendCategory(category)));
+ break;
+ }
+ }
+
+ fabCreate.setOnClickListener((View view) -> {
+ final var 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, REQUEST_CODE_CREATE_NOTE);
+ });
+ });
+ mainViewModel.getNotesListLiveData().observe(this, notes -> {
+ // https://stackoverflow.com/a/37342327
+ itemTouchHelper.attachToRecyclerView(null);
+ itemTouchHelper.attachToRecyclerView(listView);
+ adapter.setItemList(notes);
+ binding.activityNotesListView.progressCircular.setVisibility(GONE);
+ binding.activityNotesListView.emptyContentView.getRoot().setVisibility(notes.size() > 0 ? GONE : VISIBLE);
+ // Remove deleted notes from the selection
+ if (tracker.hasSelection()) {
+ final var deletedNotes = new LinkedList();
+ for (final var id : tracker.getSelection()) {
+ if (notes
+ .stream()
+ .filter(item -> !item.isSection())
+ .map(item -> (Note) item)
+ .noneMatch(item -> item.getId() == id)) {
+ deletedNotes.add(id);
+ }
+ }
+ for (final var id : deletedNotes) {
+ tracker.deselect(id);
+ }
+ }
+ });
+ mainViewModel.getSearchTerm().observe(this, adapter::setHighlightSearchQuery);
+ mainViewModel.getCategorySortingMethodOfSelectedCategory().observe(this, methodOfCategory -> {
+ updateSortMethodIcon(methodOfCategory.second);
+ activityBinding.sortingMethod.setOnClickListener((v) -> {
+ if (methodOfCategory.first != null) {
+ var newMethod = methodOfCategory.second;
+ if (newMethod == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) {
+ newMethod = CategorySortingMethod.SORT_MODIFIED_DESC;
+ } else {
+ newMethod = CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC;
+ }
+ final var modifyLiveData = mainViewModel.modifyCategoryOrder(methodOfCategory.first, newMethod);
+ modifyLiveData.observe(this, (next) -> modifyLiveData.removeObservers(this));
+ }
+ });
+ });
+ mainViewModel.getNavigationCategories().observe(this, navigationItems -> this.adapterCategories.setItems(navigationItems));
+ mainViewModel.getCurrentAccount().observe(this, (nextAccount) -> {
+ fabCreate.hide();
+ Glide
+ .with(this)
+ .load(nextAccount.getUrl() + "/index.php/avatar/" + Uri.encode(nextAccount.getUserName()) + "/64")
+ .placeholder(R.drawable.ic_account_circle_grey_24dp)
+ .error(R.drawable.ic_account_circle_grey_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(activityBinding.launchAccountSwitcher);
+
+ mainViewModel.synchronizeNotes(nextAccount, new IResponseCallback<>() {
+ @Override
+ public void onSuccess(Void v) {
+ Log.d(TAG, "Successfully synchronized notes for " + nextAccount.getAccountName());
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ if (t instanceof IntendedOfflineException) {
+ Log.i(TAG, "Capabilities and notes not updated because " + nextAccount.getAccountName() + " is offline by intention.");
+ } else if (t instanceof NetworkErrorException) {
+ BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
+ }
+ });
+ }
+ });
+ fabCreate.show();
+ activityBinding.launchAccountSwitcher.setOnClickListener((v) -> AccountSwitcherDialog.newInstance(nextAccount.getId()).show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()));
+
+ if (menuAdapter == null) {
+ 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(), resultCode);
+ }
+ });
+
+ binding.navigationMenu.setAdapter(menuAdapter);
+ } else {
+ menuAdapter.updateAccount(this, nextAccount);
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ final var accountLiveData = mainViewModel.getCurrentAccount();
+ accountLiveData.observe(this, (currentAccount) -> {
+ accountLiveData.removeObservers(this);
+ try {
+ // It is possible that after the deletion of the last account, this onResponse gets called before the ImportAccountActivity gets started.
+ if (SingleAccountHelper.getCurrentSingleSignOnAccount(this) != null) {
+ mainViewModel.synchronizeNotes(currentAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess(Void v) {
+ Log.d(TAG, "Successfully synchronized notes for " + currentAccount.getAccountName());
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ t.printStackTrace();
+ }
+ });
+ }
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } catch (NoCurrentAccountSelectedException e) {
+ Log.i(TAG, "No current account is selected - maybe the last account has been deleted?");
+ }
+ });
+ super.onResume();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mainViewModel.restoreInstanceState();
+ }
+
+ private void setupToolbars() {
+ setSupportActionBar(binding.activityNotesListView.searchToolbar);
+ activityBinding.homeToolbar.setOnClickListener((v) -> {
+ if (activityBinding.searchToolbar.getVisibility() == GONE) {
+ updateToolbars(true);
+ }
+ });
+
+ final var toggle = new ActionBarDrawerToggle(this, binding.drawerLayout, activityBinding.homeToolbar, 0, 0);
+ binding.drawerLayout.addDrawerListener(toggle);
+ toggle.syncState();
+
+ activityBinding.searchView.setOnCloseListener(() -> {
+ if (activityBinding.searchToolbar.getVisibility() == VISIBLE && TextUtils.isEmpty(activityBinding.searchView.getQuery())) {
+ updateToolbars(false);
+ return true;
+ }
+ return false;
+ });
+ activityBinding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return false;
+ }
+
+ @Override
+ public boolean onQueryTextChange(String newText) {
+ mainViewModel.postSearchTerm(newText);
+ return true;
+ }
+ });
+ }
+
+ private void setupNotesList() {
+ adapter = new ItemAdapter(this, gridView);
+ listView.setAdapter(adapter);
+ listView.setItemAnimator(null);
+ if (gridView) {
+ final int spanCount = getResources().getInteger(R.integer.grid_view_span_count);
+ final var gridLayoutManager = new StaggeredGridLayoutManager(spanCount, StaggeredGridLayoutManager.VERTICAL);
+ listView.setLayoutManager(gridLayoutManager);
+ listView.addItemDecoration(new GridItemDecoration(adapter, spanCount,
+ getResources().getDimensionPixelSize(R.dimen.spacer_3x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_5x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_3x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_1x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x)
+ ));
+ } else {
+ final var layoutManager = new LinearLayoutManager(this);
+ listView.setLayoutManager(layoutManager);
+ listView.addItemDecoration(new SectionItemDecoration(adapter,
+ getResources().getDimensionPixelSize(R.dimen.spacer_activity_sides) + getResources().getDimensionPixelSize(R.dimen.spacer_1x) + getResources().getDimensionPixelSize(R.dimen.spacer_3x) + getResources().getDimensionPixelSize(R.dimen.spacer_2x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_5x),
+ getResources().getDimensionPixelSize(R.dimen.spacer_1x),
+ 0
+ ));
+ }
+
+ listView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (dy > 0)
+ fabCreate.hide();
+ else if (dy < 0)
+ fabCreate.show();
+ }
+ });
+
+ swipeRefreshLayout.setOnRefreshListener(() -> {
+ CustomAppGlideModule.clearCache(this);
+ final var syncLiveData = mainViewModel.getCurrentAccount();
+ final Observer syncObserver = currentAccount -> {
+ syncLiveData.removeObservers(this);
+ mainViewModel.synchronizeCapabilitiesAndNotes(currentAccount, new IResponseCallback<>() {
+ @Override
+ public void onSuccess(Void v) {
+ Log.d(TAG, "Successfully synchronized capabilities and notes for " + currentAccount.getAccountName());
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ swipeRefreshLayout.setRefreshing(false);
+ if (t instanceof IntendedOfflineException) {
+ Log.i(TAG, "Capabilities and notes not updated because " + currentAccount.getAccountName() + " is offline by intention.");
+ } else if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HttpURLConnection.HTTP_UNAVAILABLE) {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_maintenance_mode, Snackbar.LENGTH_LONG).show();
+ } else if (t instanceof NetworkErrorException) {
+ BrandedSnackbar.make(coordinatorLayout, getString(R.string.error_sync, getString(R.string.error_no_network)), Snackbar.LENGTH_LONG).show();
+ } else {
+ BrandedSnackbar.make(coordinatorLayout, R.string.error_synchronization, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t)
+ .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
+ }
+ });
+ }
+ });
+ };
+ syncLiveData.observe(this, syncObserver);
+ });
+
+ tracker = ItemSelectionTracker.build(listView, adapter);
+ adapter.setTracker(tracker);
+ tracker.addObserver(new SelectionTracker.SelectionObserver() {
+ @Override
+ public void onSelectionChanged() {
+ super.onSelectionChanged();
+ if (tracker.hasSelection() && mActionMode == null) {
+ mActionMode = startSupportActionMode(new MultiSelectedActionModeCallback(MainActivity.this, coordinatorLayout, mainViewModel, MainActivity.this, canMoveNoteToAnotherAccounts, tracker, getSupportFragmentManager()));
+ }
+ if (mActionMode != null) {
+ if (tracker.hasSelection()) {
+ int selected = tracker.getSelection().size();
+ mActionMode.setTitle(getResources().getQuantityString(R.plurals.ab_selected, selected, selected));
+ } else {
+ mActionMode.finish();
+ mActionMode = null;
+ }
+ }
+ }
+ }
+ );
+
+ itemTouchHelper = new NotesListViewItemTouchHelper(this, mainViewModel, this, tracker, adapter, swipeRefreshLayout, coordinatorLayout, gridView);
+ itemTouchHelper.attachToRecyclerView(listView);
+ }
+
+ private void setupNavigationList() {
+ adapterCategories = new NavigationAdapter(this, new NavigationClickListener() {
+ @Override
+ public void onItemClick(NavigationItem item) {
+ selectItem(item, true);
+ }
+
+ private void selectItem(NavigationItem item, boolean closeNavigation) {
+ adapterCategories.setSelectedItem(item.id);
+ // update current selection
+ if (item.type != null) {
+ switch (item.type) {
+ case RECENT: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(RECENT));
+ break;
+ }
+ case FAVORITES: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(FAVORITES));
+ break;
+ }
+ case UNCATEGORIZED: {
+ mainViewModel.postSelectedCategory(new NavigationCategory(UNCATEGORIZED));
+ break;
+ }
+ default: {
+ if (item.getClass() == NavigationItem.CategoryNavigationItem.class) {
+ mainViewModel.postSelectedCategory(new NavigationCategory(((NavigationItem.CategoryNavigationItem) item).accountId, ((NavigationItem.CategoryNavigationItem) item).category));
+ } else {
+ throw new IllegalStateException(NavigationItem.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but item is not of type " + NavigationItem.CategoryNavigationItem.class.getSimpleName() + ".");
+ }
+ }
+ }
+ } else {
+ Log.e(TAG, "Unknown item navigation type. Fallback to show " + RECENT);
+ mainViewModel.postSelectedCategory(new NavigationCategory(RECENT));
+ }
+
+ if (closeNavigation) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ }
+ }
+
+ @Override
+ public void onIconClick(NavigationItem item) {
+ final var expandedCategoryLiveData = mainViewModel.getExpandedCategory();
+ expandedCategoryLiveData.observe(MainActivity.this, expandedCategory -> {
+ if (item.icon == NavigationAdapter.ICON_MULTIPLE && !item.label.equals(expandedCategory)) {
+ mainViewModel.postExpandedCategory(item.label);
+ selectItem(item, false);
+ } else if (item.icon == NavigationAdapter.ICON_MULTIPLE || item.icon == NavigationAdapter.ICON_MULTIPLE_OPEN && item.label.equals(expandedCategory)) {
+ mainViewModel.postExpandedCategory(null);
+ } else {
+ onItemClick(item);
+ }
+ expandedCategoryLiveData.removeObservers(MainActivity.this);
+ });
+ }
+ });
+ adapterCategories.setSelectedItem(ADAPTER_KEY_RECENT);
+ binding.navigationList.setAdapter(adapterCategories);
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ applyBrandToPrimaryToolbar(activityBinding.appBar, activityBinding.searchToolbar);
+ applyBrandToFAB(mainColor, textColor, activityBinding.fabCreate);
+
+ binding.headerView.setBackgroundColor(mainColor);
+ binding.appName.setTextColor(textColor);
+ activityBinding.progressCircular.getIndeterminateDrawable().setColorFilter(getSecondaryForegroundColorDependingOnTheme(this, mainColor), PorterDuff.Mode.SRC_IN);
+
+ // TODO We assume, that the background of the spinner is always white
+ activityBinding.swiperefreshlayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : Color.BLACK);
+ binding.appName.setTextColor(textColor);
+ DrawableCompat.setTint(binding.logo.getDrawable(), textColor);
+
+ adapter.applyBrand(mainColor, textColor);
+ adapterCategories.applyBrand(mainColor, textColor);
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ if (activityBinding.searchToolbar.getVisibility() == VISIBLE) {
+ updateToolbars(false);
+ return true;
+ } else {
+ return super.onSupportNavigateUp();
+ }
+ }
+
+ /**
+ * Updates sorting method icon.
+ */
+ private void updateSortMethodIcon(CategorySortingMethod method) {
+ if (method == CategorySortingMethod.SORT_LEXICOGRAPHICAL_ASC) {
+ activityBinding.sortingMethod.setImageResource(R.drawable.alphabetical_asc);
+ activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_last_modified));
+ if (SDK_INT >= O) {
+ activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_last_modified));
+ }
+ } else {
+ activityBinding.sortingMethod.setImageResource(R.drawable.modification_desc);
+ activityBinding.sortingMethod.setContentDescription(getString(R.string.sort_alphabetically));
+ if (SDK_INT >= O) {
+ activityBinding.sortingMethod.setTooltipText(getString(R.string.sort_alphabetically));
+ }
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
+ activityBinding.searchView.setQuery(intent.getStringExtra(SearchManager.QUERY), true);
+ }
+ super.onNewIntent(intent);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
+ }
+
+ /**
+ * Handles the Results of started Sub Activities (Created Note, Edited Note)
+ *
+ * @param requestCode int to distinguish between the different Sub Activities
+ * @param resultCode int Return Code
+ * @param data Intent
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ switch (requestCode) {
+ case REQUEST_CODE_CREATE_NOTE: {
+ listView.scrollToPosition(0);
+ break;
+ }
+ 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
+ if (RESULT_OK == resultCode) {
+ ActivityCompat.recreate(this);
+ return;
+ }
+ break;
+ }
+ default: {
+ try {
+ AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> {
+ CapabilitiesWorker.update(this);
+ executor.submit(() -> {
+ final var importSnackbar = BrandedSnackbar.make(coordinatorLayout, R.string.progress_import_indeterminate, Snackbar.LENGTH_INDEFINITE);
+ Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId);
+ try {
+ Log.i(TAG, "Refreshing capabilities for " + ssoAccount.name);
+ final var capabilities = CapabilitiesClient.getCapabilities(getApplicationContext(), ssoAccount, null, ApiProvider.getInstance());
+ final String displayName = CapabilitiesClient.getDisplayName(getApplicationContext(), ssoAccount, ApiProvider.getInstance());
+ final var status$ = mainViewModel.addAccount(ssoAccount.url, ssoAccount.userId, ssoAccount.name, capabilities, displayName, new IResponseCallback() {
+ @Override
+ public void onSuccess(Account result) {
+ executor.submit(() -> {
+ runOnUiThread(() -> {
+ importSnackbar.setText(R.string.account_imported);
+ importSnackbar.setAction(R.string.simple_switch, (v) -> mainViewModel.postCurrentAccount(mainViewModel.getLocalAccountByAccountName(ssoAccount.name)));
+ });
+ Log.i(TAG, capabilities.toString());
+ });
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ runOnUiThread(() -> {
+ importSnackbar.dismiss();
+ ExceptionDialogFragment.newInstance(t).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
+ }
+ });
+ runOnUiThread(() -> status$.observe(this, (status) -> {
+ importSnackbar.show();
+ Log.v(TAG, "Status: " + status.count + " of " + status.total);
+ if (status.count > 0) {
+ importSnackbar.setText(getString(R.string.progress_import, status.count + 1, status.total));
+ }
+ }));
+ } catch (Throwable e) {
+ importSnackbar.dismiss();
+ 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.");
+ runOnUiThread(() -> {
+ mainViewModel.postCurrentAccount(mainViewModel.getLocalAccountByAccountName(ssoAccount.name));
+ // 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() != 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 {
+ e.printStackTrace();
+ runOnUiThread(() -> {
+ binding.activityNotesListView.progressCircular.setVisibility(GONE);
+ ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ });
+ }
+ }
+ });
+ });
+ } catch (AccountImportCancelledException e) {
+ Log.i(TAG, "AccountImport has been cancelled.");
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onNoteClick(int position, View v) {
+ final boolean hasCheckedItems = tracker.getSelection().size() > 0;
+ if (!hasCheckedItems) {
+ final var note = (Note) adapter.getItem(position);
+ startActivity(new Intent(getApplicationContext(), EditNoteActivity.class)
+ .putExtra(EditNoteActivity.PARAM_NOTE_ID, note.getId()));
+ }
+ }
+
+ @Override
+ public void onNoteFavoriteClick(int position, View view) {
+ final var toggleLiveData = mainViewModel.toggleFavoriteAndSync(((Note) adapter.getItem(position)).getId());
+ toggleLiveData.observe(this, (next) -> toggleLiveData.removeObservers(this));
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (activityBinding.searchToolbar.getVisibility() == VISIBLE) {
+ updateToolbars(false);
+ } else if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void updateToolbars(boolean enableSearch) {
+ activityBinding.homeToolbar.setVisibility(enableSearch ? GONE : VISIBLE);
+ activityBinding.searchToolbar.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);
+ }
+ }
+
+ @Override
+ public void addAccount() {
+ askForNewAccount(this);
+ }
+
+ @Override
+ public void onAccountChosen(@NonNull Account localAccount) {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ mainViewModel.postCurrentAccount(localAccount);
+ }
+
+ @Override
+ public void onAccountPicked(@NonNull Account account) {
+ for (final var noteId : tracker.getSelection()) {
+ final var moveLiveData = mainViewModel.moveNoteToAnotherAccount(account, noteId);
+ moveLiveData.observe(this, (v) -> {
+ tracker.deselect(noteId);
+ moveLiveData.removeObservers(this);
+ });
+ }
+ }
+
+ @Override
+ public void onCategoryChosen(String category) {
+ final var categoryLiveData = mainViewModel.setCategory(tracker.getSelection(), category);
+ categoryLiveData.observe(this, (next) -> categoryLiveData.removeObservers(this));
+ tracker.clearSelection();
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..4de6ebeced78dbff5d1edcefcf1f82f45fc69d10
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -0,0 +1,666 @@
+package it.niedermann.owncloud.notes.main;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
+import static it.niedermann.owncloud.notes.main.MainActivity.ADAPTER_KEY_RECENT;
+import static it.niedermann.owncloud.notes.main.MainActivity.ADAPTER_KEY_STARRED;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByCategory;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByInitials;
+import static it.niedermann.owncloud.notes.main.slots.SlotterUtil.fillListByTime;
+import static it.niedermann.owncloud.notes.shared.model.CategorySortingMethod.SORT_MODIFIED_DESC;
+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;
+import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
+import static it.niedermann.owncloud.notes.shared.util.DisplayUtils.convertToCategoryNavigationItem;
+
+import android.accounts.NetworkErrorException;
+import android.app.Application;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.SavedStateHandle;
+
+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.exceptions.UnknownErrorException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+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;
+import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.persistence.entity.SingleNoteWidgetData;
+import it.niedermann.owncloud.notes.shared.model.Capabilities;
+import it.niedermann.owncloud.notes.shared.model.CategorySortingMethod;
+import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
+import it.niedermann.owncloud.notes.shared.model.ImportStatus;
+import it.niedermann.owncloud.notes.shared.model.Item;
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
+
+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";
+ private static final String KEY_SEARCH_TERM = "searchTerm";
+ private static final String KEY_SELECTED_CATEGORY = "selectedCategory";
+ private static final String KEY_EXPANDED_CATEGORY = "expandedCategory";
+
+ @NonNull
+ private final NotesRepository repo;
+
+ @NonNull
+ private final MutableLiveData currentAccount = new MutableLiveData<>();
+ @NonNull
+ private final MutableLiveData searchTerm = new MutableLiveData<>(null);
+ @NonNull
+ private final MutableLiveData selectedCategory = new MutableLiveData<>(new NavigationCategory(RECENT));
+ @NonNull
+ private final MutableLiveData expandedCategory = new MutableLiveData<>(null);
+
+ public MainViewModel(@NonNull Application application, @NonNull SavedStateHandle savedStateHandle) {
+ super(application);
+ this.repo = NotesRepository.getInstance(application);
+ this.state = savedStateHandle;
+ }
+
+ public void restoreInstanceState() {
+ Log.v(TAG, "[restoreInstanceState]");
+ final Account account = state.get(KEY_CURRENT_ACCOUNT);
+ if (account != null) {
+ postCurrentAccount(account);
+ }
+ postSearchTerm(state.get(KEY_SEARCH_TERM));
+ final NavigationCategory selectedCategory = state.get(KEY_SELECTED_CATEGORY);
+ if (selectedCategory != null) {
+ postSelectedCategory(selectedCategory);
+ Log.v(TAG, "[restoreInstanceState] - selectedCategory: " + selectedCategory);
+ }
+ postExpandedCategory(state.get(KEY_EXPANDED_CATEGORY));
+ }
+
+ @NonNull
+ public LiveData getCurrentAccount() {
+ return distinctUntilChanged(currentAccount);
+ }
+
+ public void postCurrentAccount(@NonNull Account account) {
+ state.set(KEY_CURRENT_ACCOUNT, account);
+ BrandingUtil.saveBrandColors(getApplication(), account.getColor(), account.getTextColor());
+ SingleAccountHelper.setCurrentAccount(getApplication(), account.getAccountName());
+
+ final var currentAccount = this.currentAccount.getValue();
+ // If only ETag or colors change, we must not reset the navigation
+ // TODO in the long term we should store the last NavigationCategory for each Account
+ if (currentAccount == null || currentAccount.getId() != account.getId()) {
+ this.currentAccount.setValue(account);
+ this.searchTerm.setValue("");
+ this.selectedCategory.setValue(new NavigationCategory(RECENT));
+ }
+ }
+
+ @NonNull
+ public LiveData getSearchTerm() {
+ return distinctUntilChanged(searchTerm);
+ }
+
+ public void postSearchTerm(String searchTerm) {
+ state.set(KEY_SEARCH_TERM, searchTerm);
+ this.searchTerm.postValue(searchTerm);
+ }
+
+ @NonNull
+ public LiveData getSelectedCategory() {
+ return distinctUntilChanged(selectedCategory);
+ }
+
+ public void postSelectedCategory(@NonNull NavigationCategory selectedCategory) {
+ state.set(KEY_SELECTED_CATEGORY, selectedCategory);
+ Log.v(TAG, "[postSelectedCategory] - selectedCategory: " + selectedCategory);
+ this.selectedCategory.postValue(selectedCategory);
+
+ // Close sub categories
+ switch (selectedCategory.getType()) {
+ case RECENT:
+ case FAVORITES:
+ case UNCATEGORIZED: {
+ postExpandedCategory(null);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category == null) {
+ postExpandedCategory(null);
+ Log.e(TAG, "navigation selection is a " + DEFAULT_CATEGORY + ", but the contained category is null.");
+ } else {
+ int slashIndex = category.indexOf('/');
+ final String rootCategory = slashIndex < 0 ? category : category.substring(0, slashIndex);
+ final String expandedCategory = getExpandedCategory().getValue();
+ if (expandedCategory != null && !expandedCategory.equals(rootCategory)) {
+ postExpandedCategory(null);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData> getCategorySortingMethodOfSelectedCategory() {
+ return switchMap(getSelectedCategory(), selectedCategory -> map(repo.getCategoryOrder(selectedCategory), sortingMethod -> new Pair<>(selectedCategory, sortingMethod)));
+ }
+
+ public LiveData modifyCategoryOrder(@NonNull NavigationCategory selectedCategory, @NonNull CategorySortingMethod sortingMethod) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[modifyCategoryOrder] - currentAccount: " + currentAccount.getAccountName());
+ repo.modifyCategoryOrder(currentAccount.getId(), selectedCategory, sortingMethod);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public void postExpandedCategory(@Nullable String expandedCategory) {
+ state.set(KEY_EXPANDED_CATEGORY, expandedCategory);
+ this.expandedCategory.postValue(expandedCategory);
+ }
+
+ @NonNull
+ public LiveData getExpandedCategory() {
+ return distinctUntilChanged(expandedCategory);
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData> getNotesListLiveData() {
+ final var insufficientInformation = new MutableLiveData>();
+ return distinctUntilChanged(switchMap(getCurrentAccount(), currentAccount -> {
+ Log.v(TAG, "[getNotesListLiveData] - currentAccount: " + currentAccount);
+ if (currentAccount == null) {
+ return insufficientInformation;
+ } else {
+ return switchMap(getSelectedCategory(), selectedCategory -> {
+ if (selectedCategory == null) {
+ return insufficientInformation;
+ } else {
+ Log.v(TAG, "[getNotesListLiveData] - selectedCategory: " + selectedCategory);
+ return switchMap(getSearchTerm(), 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() + "%";
+ Log.v(TAG, "[getNotesListLiveData] - sortMethod: " + sortingMethod.second);
+ final LiveData> fromDatabase;
+ switch (selectedCategory.getType()) {
+ case RECENT: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + RECENT);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? repo.searchRecentByModified$(accountId, searchQueryOrWildcard)
+ : repo.searchRecentLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case FAVORITES: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + FAVORITES);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? repo.searchFavoritesByModified$(accountId, searchQueryOrWildcard)
+ : repo.searchFavoritesLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case UNCATEGORIZED: {
+ Log.v(TAG, "[getNotesListLiveData] - category: " + UNCATEGORIZED);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? repo.searchUncategorizedByModified$(accountId, searchQueryOrWildcard)
+ : repo.searchUncategorizedLexicographically$(accountId, searchQueryOrWildcard);
+ break;
+ }
+ case DEFAULT_CATEGORY:
+ default: {
+ final String category = selectedCategory.getCategory();
+ if (category == null) {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ Log.v(TAG, "[getNotesListLiveData] - category: " + category);
+ fromDatabase = sortingMethod.second == SORT_MODIFIED_DESC
+ ? repo.searchCategoryByModified$(accountId, searchQueryOrWildcard, category)
+ : repo.searchCategoryLexicographically$(accountId, searchQueryOrWildcard, category);
+ break;
+ }
+ }
+
+ Log.v(TAG, "[getNotesListLiveData] - -------------------------------------");
+ return distinctUntilChanged(map(fromDatabase, noteList -> fromNotes(noteList, selectedCategory, sortingMethod.second)));
+ });
+ });
+ }
+ });
+ }
+ }));
+ }
+
+ private List- fromNotes(List noteList, @NonNull NavigationCategory selectedCategory, @Nullable CategorySortingMethod sortingMethod) {
+ if (selectedCategory.getType() == DEFAULT_CATEGORY) {
+ final String category = selectedCategory.getCategory();
+ if (category != null) {
+ return fillListByCategory(noteList, category);
+ } else {
+ throw new IllegalStateException(NavigationCategory.class.getSimpleName() + " type is " + DEFAULT_CATEGORY + ", but category is null.");
+ }
+ }
+ if (sortingMethod == SORT_MODIFIED_DESC) {
+ return fillListByTime(getApplication(), noteList);
+ } else {
+ return fillListByInitials(getApplication(), noteList);
+ }
+ }
+
+ @NonNull
+ @MainThread
+ public LiveData
> getNavigationCategories() {
+ final var insufficientInformation = new MutableLiveData>();
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return insufficientInformation;
+ } else {
+ Log.v(TAG, "[getNavigationCategories] - currentAccount: " + currentAccount.getAccountName());
+ return switchMap(getExpandedCategory(), expandedCategory -> {
+ Log.v(TAG, "[getNavigationCategories] - expandedCategory: " + expandedCategory);
+ return switchMap(repo.count$(currentAccount.getId()), (count) -> {
+ Log.v(TAG, "[getNavigationCategories] - count: " + count);
+ return switchMap(repo.countFavorites$(currentAccount.getId()), (favoritesCount) -> {
+ Log.v(TAG, "[getNavigationCategories] - favoritesCount: " + favoritesCount);
+ return distinctUntilChanged(map(repo.getCategories$(currentAccount.getId()), fromDatabase ->
+ fromCategoriesWithNotesCount(getApplication(), expandedCategory, fromDatabase, count, favoritesCount)
+ ));
+ });
+ });
+ });
+ }
+ });
+ }
+
+ private static List fromCategoriesWithNotesCount(@NonNull Context context, @Nullable String expandedCategory, @NonNull List fromDatabase, int count, int favoritesCount) {
+ final var categories = convertToCategoryNavigationItem(context, fromDatabase);
+ final var itemRecent = new NavigationItem(ADAPTER_KEY_RECENT, context.getString(R.string.label_all_notes), count, R.drawable.ic_access_time_grey600_24dp, RECENT);
+ final var itemFavorites = new NavigationItem(ADAPTER_KEY_STARRED, context.getString(R.string.label_favorites), favoritesCount, R.drawable.ic_star_yellow_24dp, FAVORITES);
+
+ final var items = new ArrayList(fromDatabase.size() + 3);
+ items.add(itemRecent);
+ items.add(itemFavorites);
+ NavigationItem lastPrimaryCategory = null;
+ NavigationItem lastSecondaryCategory = null;
+ for (final var item : categories) {
+ final int slashIndex = item.label.indexOf('/');
+ final String currentPrimaryCategory = slashIndex < 0 ? item.label : item.label.substring(0, slashIndex);
+ final boolean isCategoryOpen = currentPrimaryCategory.equals(expandedCategory);
+ String currentSecondaryCategory = null;
+
+ if (isCategoryOpen && !currentPrimaryCategory.equals(item.label)) {
+ final String currentCategorySuffix = item.label.substring(expandedCategory.length() + 1);
+ final int subSlashIndex = currentCategorySuffix.indexOf('/');
+ currentSecondaryCategory = subSlashIndex < 0 ? currentCategorySuffix : currentCategorySuffix.substring(0, subSlashIndex);
+ }
+
+ boolean belongsToLastPrimaryCategory = lastPrimaryCategory != null && currentPrimaryCategory.equals(lastPrimaryCategory.label);
+ final boolean belongsToLastSecondaryCategory = belongsToLastPrimaryCategory && lastSecondaryCategory != null && lastSecondaryCategory.label.equals(currentSecondaryCategory);
+
+ if (isCategoryOpen && !belongsToLastPrimaryCategory && currentSecondaryCategory != null) {
+ lastPrimaryCategory = new NavigationItem("category:" + currentPrimaryCategory, currentPrimaryCategory, 0, NavigationAdapter.ICON_MULTIPLE_OPEN);
+ items.add(lastPrimaryCategory);
+ belongsToLastPrimaryCategory = true;
+ }
+
+ if (belongsToLastPrimaryCategory && belongsToLastSecondaryCategory) {
+ lastSecondaryCategory.count += item.count;
+ lastSecondaryCategory.icon = NavigationAdapter.ICON_SUB_MULTIPLE;
+ } else if (belongsToLastPrimaryCategory) {
+ if (isCategoryOpen) {
+ if (currentSecondaryCategory == null) {
+ throw new IllegalStateException("Current secondary category is null. Last primary category: " + lastPrimaryCategory);
+ }
+ item.label = currentSecondaryCategory;
+ item.id = "category:" + item.label;
+ item.icon = NavigationAdapter.ICON_SUB_FOLDER;
+ items.add(item);
+ lastSecondaryCategory = item;
+ } else {
+ lastPrimaryCategory.count += item.count;
+ lastPrimaryCategory.icon = NavigationAdapter.ICON_MULTIPLE;
+ lastSecondaryCategory = null;
+ }
+ } else {
+ if (isCategoryOpen) {
+ item.icon = NavigationAdapter.ICON_MULTIPLE_OPEN;
+ } else {
+ item.label = currentPrimaryCategory;
+ item.id = "category:" + item.label;
+ }
+ items.add(item);
+ lastPrimaryCategory = item;
+ lastSecondaryCategory = null;
+ }
+ }
+ return items;
+ }
+
+ public void synchronizeCapabilitiesAndNotes(@NonNull Account localAccount, @NonNull IResponseCallback callback) {
+ Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize capabilities for " + localAccount.getAccountName());
+ synchronizeCapabilities(localAccount, new IResponseCallback() {
+ @Override
+ public void onSuccess(Void v) {
+ Log.i(TAG, "[synchronizeCapabilitiesAndNotes] Synchronize notes for " + localAccount.getAccountName());
+ synchronizeNotes(localAccount, callback);
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {
+ callback.onError(t);
+ }
+ });
+ }
+
+ /**
+ * 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 callback) {
+ executor.submit(() -> {
+ if (!repo.isSyncPossible()) {
+ repo.updateNetworkStatus();
+ }
+ if (repo.isSyncPossible()) {
+ try {
+ final var ssoAccount = AccountImporter.getSingleSignOnAccount(getApplication(), localAccount.getAccountName());
+ try {
+ final var 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);
+ }
+ } else {
+ if (repo.isNetworkConnected() && repo.isSyncOnlyOnWifi()) {
+ callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
+ } else {
+ callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
+ }
+ }
+ }, "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 callback) {
+ executor.submit(() -> {
+ Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
+ if (!repo.isSyncPossible()) {
+ repo.updateNetworkStatus();
+ }
+ if (repo.isSyncPossible()) {
+ repo.scheduleSync(currentAccount, false);
+ callback.onSuccess(null);
+ } else { // Sync is not possible
+ if (repo.isNetworkConnected() && repo.isSyncOnlyOnWifi()) {
+ callback.onError(new IntendedOfflineException("Network is connected, but sync is not possible."));
+ } else {
+ callback.onError(new NetworkErrorException("Sync is not possible, because network is not connected."));
+ }
+ }
+ }, "SYNC_NOTES");
+ }
+
+ public LiveData getSyncStatus() {
+ return repo.getSyncStatus();
+ }
+
+ public LiveData> getSyncErrors() {
+ return repo.getSyncErrors();
+ }
+
+ public LiveData hasMultipleAccountsConfigured() {
+ return map(repo.countAccounts$(), (counter) -> counter != null && counter > 1);
+ }
+
+ @WorkerThread
+ public Account getLocalAccountByAccountName(String accountName) {
+ return repo.getAccountByName(accountName);
+ }
+
+ @WorkerThread
+ public List getAccounts() {
+ return repo.getAccounts();
+ }
+
+ public LiveData setCategory(Iterable noteIds, @NonNull String category) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[setCategory] - currentAccount: " + currentAccount.getAccountName());
+ for (Long noteId : noteIds) {
+ repo.setCategory(currentAccount, noteId, category);
+ }
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData moveNoteToAnotherAccount(Account account, long noteId) {
+ return switchMap(repo.getNoteById$(noteId), (note) -> {
+ Log.v(TAG, "[moveNoteToAnotherAccount] - note: " + (BuildConfig.DEBUG ? note : note.getTitle()));
+ return repo.moveNoteToAnotherAccount(account, note);
+ });
+ }
+
+ public LiveData toggleFavoriteAndSync(long noteId) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[toggleFavoriteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ repo.toggleFavoriteAndSync(currentAccount, noteId);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData deleteNoteAndSync(long id) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[deleteNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ repo.deleteNoteAndSync(currentAccount, id);
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData deleteNotesAndSync(@NonNull Collection ids) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>(null);
+ } else {
+ Log.v(TAG, "[deleteNotesAndSync] - currentAccount: " + currentAccount.getAccountName());
+ for (final var id : ids) {
+ repo.deleteNoteAndSync(currentAccount, id);
+ }
+ return new MutableLiveData<>(null);
+ }
+ });
+ }
+
+ public LiveData addAccount(@NonNull String url, @NonNull String username, @NonNull String accountName, @NonNull Capabilities capabilities, @Nullable String displayName, @NonNull IResponseCallback callback) {
+ return repo.addAccount(url, username, accountName, capabilities, displayName, callback);
+ }
+
+ public LiveData getFullNote$(long id) {
+ return map(getFullNotesWithCategory(Collections.singleton(id)), input -> input.get(0));
+ }
+
+ @WorkerThread
+ public Note getFullNote(long id) {
+ return repo.getNoteById(id);
+ }
+
+ public LiveData> getFullNotesWithCategory(@NonNull Collection ids) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>();
+ } else {
+ Log.v(TAG, "[getNote] - currentAccount: " + currentAccount.getAccountName());
+ final var notes = new MutableLiveData>();
+ executor.submit(() -> notes.postValue(
+ ids
+ .stream()
+ .map(repo::getNoteById)
+ .collect(Collectors.toList())
+ ));
+ return notes;
+ }
+ });
+ }
+
+ public LiveData addNoteAndSync(Note note) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount == null) {
+ return new MutableLiveData<>();
+ } else {
+ Log.v(TAG, "[addNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ return repo.addNoteAndSync(currentAccount, note);
+ }
+ });
+ }
+
+ public LiveData updateNoteAndSync(@NonNull Note oldNote, @Nullable String newContent, @Nullable String newTitle) {
+ return switchMap(getCurrentAccount(), currentAccount -> {
+ if (currentAccount != null) {
+ Log.v(TAG, "[updateNoteAndSync] - currentAccount: " + currentAccount.getAccountName());
+ repo.updateNoteAndSync(currentAccount, oldNote, newContent, newTitle, null);
+ }
+ return new MutableLiveData<>(null);
+ });
+ }
+
+ public void createOrUpdateSingleNoteWidgetData(SingleNoteWidgetData data) {
+ repo.createOrUpdateSingleNoteWidgetData(data);
+ }
+
+ public List getLocalModifiedNotes(long accountId) {
+ return repo.getLocalModifiedNotes(accountId);
+ }
+
+ public LiveData getAccountsCount() {
+ return repo.countAccounts$();
+ }
+
+ @WorkerThread
+ public String collectNoteContents(@NonNull List noteIds) {
+ final var noteContents = new StringBuilder();
+ for (final var noteId : noteIds) {
+ final var fullNote = repo.getNoteById(noteId);
+ final String tempFullNote = fullNote.getContent();
+ if (!TextUtils.isEmpty(tempFullNote)) {
+ if (noteContents.length() > 0) {
+ noteContents.append("\n\n");
+ }
+ noteContents.append(tempFullNote);
+ }
+ }
+ return noteContents.toString();
+ }
+
+ /**
+ * @return true
if {@param exceptions} contains at least one exception which is not caused by flaky infrastructure.
+ * @see Issue #1303
+ */
+ public boolean containsNonInfrastructureRelatedItems(@Nullable Collection exceptions) {
+ if (exceptions == null || exceptions.isEmpty()) {
+ return false;
+ }
+
+ return exceptions.stream().anyMatch(e -> !exceptionIsInfrastructureRelated(e));
+ }
+
+ private boolean exceptionIsInfrastructureRelated(@Nullable Throwable e) {
+ if (e == null) {
+ return false;
+ }
+
+ if (e instanceof RuntimeException || e instanceof UnknownErrorException) {
+ if (isSoftwareCausedConnectionAbort(e.getMessage()) || isNetworkUnreachable(e.getMessage())) {
+ return true;
+ }
+ }
+
+ return exceptionIsInfrastructureRelated(e.getCause());
+ }
+
+ private boolean isSoftwareCausedConnectionAbort(@Nullable String input) {
+ if (input == null) {
+ return false;
+ }
+ return input.toLowerCase(Locale.ROOT).contains("software caused connection abort");
+ }
+
+ private boolean isNetworkUnreachable(@Nullable String input) {
+ if (input == null) {
+ return false;
+ }
+ final var lower = input.toLowerCase(Locale.ROOT);
+ return lower.contains("failed to connect") && lower.contains("network is unreachable");
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..a76b20684417c86d06e3f2970169d25170afc422
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MultiSelectedActionModeCallback.java
@@ -0,0 +1,174 @@
+package it.niedermann.owncloud.notes.main;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.view.ActionMode;
+import androidx.appcompat.view.ActionMode.Callback;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.selection.SelectionTracker;
+
+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;
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
+import it.niedermann.owncloud.notes.edit.category.CategoryDialogFragment;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+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
+ private final Context context;
+ @NonNull
+ private final View view;
+ @NonNull
+ private final MainViewModel mainViewModel;
+ @NonNull
+ private final LifecycleOwner lifecycleOwner;
+ private final boolean canMoveNoteToAnotherAccounts;
+ @NonNull
+ private final SelectionTracker tracker;
+ @NonNull
+ private final FragmentManager fragmentManager;
+
+ public MultiSelectedActionModeCallback(
+ @NonNull Context context, @NonNull View view, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, boolean canMoveNoteToAnotherAccounts, @NonNull SelectionTracker tracker, @NonNull FragmentManager fragmentManager) {
+ this.context = context;
+ this.view = view;
+ this.mainViewModel = mainViewModel;
+ this.lifecycleOwner = lifecycleOwner;
+ this.canMoveNoteToAnotherAccounts = canMoveNoteToAnotherAccounts;
+ this.tracker = tracker;
+ this.fragmentManager = fragmentManager;
+
+ final TypedValue typedValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.colorAccent, typedValue, true);
+ colorAccent = typedValue.data;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // inflate contextual menu
+ mode.getMenuInflater().inflate(R.menu.menu_list_context_multiple, menu);
+ menu.findItem(R.id.menu_move).setVisible(canMoveNoteToAnotherAccounts);
+ for (int i = 0; i < menu.size(); i++) {
+ var drawable = menu.getItem(i).getIcon();
+ if (drawable != null) {
+ drawable = DrawableCompat.wrap(drawable);
+ DrawableCompat.setTint(drawable, colorAccent);
+ menu.getItem(i).setIcon(drawable);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ /**
+ * @param mode ActionMode - used to close the Action Bar after all work is done.
+ * @param item MenuItem - the item in the List that contains the Node
+ * @return boolean
+ */
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_delete) {
+ final var selection = new ArrayList(tracker.getSelection().size());
+ for (final var sel : tracker.getSelection()) {
+ selection.add(sel);
+ }
+ final var fullNotes$ = mainViewModel.getFullNotesWithCategory(selection);
+ fullNotes$.observe(lifecycleOwner, (fullNotes) -> {
+ fullNotes$.removeObservers(lifecycleOwner);
+ tracker.clearSelection();
+ final var deleteLiveData = mainViewModel.deleteNotesAndSync(selection);
+ deleteLiveData.observe(lifecycleOwner, (next) -> deleteLiveData.removeObservers(lifecycleOwner));
+ final String deletedSnackbarTitle = fullNotes.size() == 1
+ ? context.getString(R.string.action_note_deleted, fullNotes.get(0).getTitle())
+ : context.getResources().getQuantityString(R.plurals.bulk_notes_deleted, fullNotes.size(), fullNotes.size());
+ BrandedSnackbar.make(view, deletedSnackbarTitle, Snackbar.LENGTH_LONG)
+ .setAction(R.string.action_undo, (View v) -> {
+ for (final var deletedNote : fullNotes) {
+ final var undoLiveData = mainViewModel.addNoteAndSync(deletedNote);
+ undoLiveData.observe(lifecycleOwner, (o) -> undoLiveData.removeObservers(lifecycleOwner));
+ }
+ String restoreSnackbarTitle = fullNotes.size() == 1
+ ? context.getString(R.string.action_note_restored, fullNotes.get(0).getTitle())
+ : context.getResources().getQuantityString(R.plurals.bulk_notes_restored, fullNotes.size(), fullNotes.size());
+ BrandedSnackbar.make(view, restoreSnackbarTitle, Snackbar.LENGTH_SHORT)
+ .show();
+ })
+ .show();
+ });
+ return true;
+ } else if (itemId == R.id.menu_move) {
+ final var currentAccount$ = mainViewModel.getCurrentAccount();
+ currentAccount$.observe(lifecycleOwner, account -> {
+ currentAccount$.removeObservers(lifecycleOwner);
+ executor.submit(() -> AccountPickerDialogFragment
+ .newInstance(new ArrayList<>(mainViewModel.getAccounts()), account.getId())
+ .show(fragmentManager, AccountPickerDialogFragment.class.getSimpleName()));
+ });
+ return true;
+ } else if (itemId == R.id.menu_share) {
+ final var selection = new ArrayList(tracker.getSelection().size());
+ for (final var sel : tracker.getSelection()) {
+ selection.add(sel);
+ }
+ tracker.clearSelection();
+
+ executor.submit(() -> {
+ if (selection.size() == 1) {
+ final var note = mainViewModel.getFullNote(selection.get(0));
+ ShareUtil.openShareDialog(context, note.getTitle(), note.getContent());
+ } else {
+ ShareUtil.openShareDialog(context,
+ context.getResources().getQuantityString(R.plurals.share_multiple, selection.size(), selection.size()),
+ mainViewModel.collectNoteContents(selection));
+ }
+ });
+ return true;
+ } else if (itemId == R.id.menu_category) {// TODO detect whether all selected notes do have the same category - in this case preselect it
+ final var accountLiveData = mainViewModel.getCurrentAccount();
+ accountLiveData.observe(lifecycleOwner, account -> {
+ accountLiveData.removeObservers(lifecycleOwner);
+ CategoryDialogFragment
+ .newInstance(account.getId(), "")
+ .show(fragmentManager, CategoryDialogFragment.class.getSimpleName());
+ });
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ if (mode != null) {
+ mode.finish();
+ }
+ tracker.clearSelection();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7470fa2579b70efdab5574db3b33c8899c5143db
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/ItemAdapter.java
@@ -0,0 +1,242 @@
+package it.niedermann.owncloud.notes.main.items;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.selection.SelectionTracker;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.Branded;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding;
+import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolder;
+import it.niedermann.owncloud.notes.main.items.grid.NoteViewGridHolderOnlyTitle;
+import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithExcerpt;
+import it.niedermann.owncloud.notes.main.items.list.NoteViewHolderWithoutExcerpt;
+import it.niedermann.owncloud.notes.main.items.section.SectionItem;
+import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.Item;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
+
+public class ItemAdapter extends RecyclerView.Adapter implements Branded {
+
+ private static final String TAG = ItemAdapter.class.getSimpleName();
+
+ public static final int TYPE_SECTION = 0;
+ public static final int TYPE_NOTE_WITH_EXCERPT = 1;
+ public static final int TYPE_NOTE_WITHOUT_EXCERPT = 2;
+ public static final int TYPE_NOTE_ONLY_TITLE = 3;
+
+ private final NoteClickListener noteClickListener;
+ private final boolean gridView;
+ @NonNull
+ private final List- itemList = new ArrayList<>();
+ private boolean showCategory = true;
+ private CharSequence searchQuery;
+ private SelectionTracker tracker = null;
+ @Px
+ private final float fontSize;
+ private final boolean monospace;
+ @ColorInt
+ private int mainColor;
+ @ColorInt
+ private int textColor;
+ @Nullable
+ private Integer swipedPosition;
+
+ public ItemAdapter(@NonNull T context, boolean gridView) {
+ this.noteClickListener = context;
+ this.gridView = gridView;
+ this.mainColor = ContextCompat.getColor(context, R.color.defaultBrand);
+ this.textColor = Color.WHITE;
+ final var sp = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ this.fontSize = getFontSizeFromPreferences(context, sp);
+ this.monospace = sp.getBoolean(context.getString(R.string.pref_key_font), false);
+ setHasStableIds(true);
+ }
+
+
+ // FIXME this causes {@link it.niedermann.owncloud.notes.noteslist.items.list.NotesListViewItemTouchHelper} to not call clearView anymore → After marking a note as favorite, it stays yellow.
+ @Override
+ public long getItemId(int position) {
+ return getItemViewType(position) == TYPE_SECTION
+ ? ((SectionItem) getItem(position)).getTitle().hashCode() * -1
+ : ((Note) getItem(position)).getId();
+ }
+
+ /**
+ * Updates the item list and notifies respective view to update.
+ *
+ * @param itemList List of items to be set
+ */
+ public void setItemList(@NonNull List
- itemList) {
+ this.itemList.clear();
+ this.itemList.addAll(itemList);
+ this.swipedPosition = null;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ if (gridView) {
+ switch (viewType) {
+ case TYPE_SECTION: {
+ return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(inflater));
+ }
+ case TYPE_NOTE_ONLY_TITLE: {
+ return new NoteViewGridHolderOnlyTitle(ItemNotesListNoteItemGridOnlyTitleBinding.inflate(inflater, parent, false), noteClickListener, monospace, fontSize);
+ }
+ case TYPE_NOTE_WITH_EXCERPT:
+ case TYPE_NOTE_WITHOUT_EXCERPT: {
+ return new NoteViewGridHolder(ItemNotesListNoteItemGridBinding.inflate(inflater, parent, false), noteClickListener, monospace, fontSize);
+ }
+ default: {
+ throw new IllegalArgumentException("Not supported viewType: " + viewType);
+ }
+ }
+ } else {
+ switch (viewType) {
+ case TYPE_SECTION: {
+ return new SectionViewHolder(ItemNotesListSectionItemBinding.inflate(inflater));
+ }
+ case TYPE_NOTE_WITH_EXCERPT: {
+ return new NoteViewHolderWithExcerpt(ItemNotesListNoteItemWithExcerptBinding.inflate(inflater, parent, false), noteClickListener);
+ }
+ case TYPE_NOTE_ONLY_TITLE:
+ case TYPE_NOTE_WITHOUT_EXCERPT: {
+ return new NoteViewHolderWithoutExcerpt(ItemNotesListNoteItemWithoutExcerptBinding.inflate(inflater, parent, false), noteClickListener);
+ }
+ default: {
+ throw new IllegalArgumentException("Not supported viewType: " + viewType);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
+ boolean isSelected = false;
+ if (tracker != null) {
+ final Long itemId = getItemId(position);
+ if (tracker.isSelected(itemId)) {
+ tracker.select(itemId);
+ isSelected = true;
+ } else {
+ tracker.deselect(itemId);
+ }
+ }
+ switch (getItemViewType(position)) {
+ case TYPE_SECTION: {
+ ((SectionViewHolder) holder).bind((SectionItem) itemList.get(position));
+ break;
+ }
+ case TYPE_NOTE_WITH_EXCERPT:
+ case TYPE_NOTE_WITHOUT_EXCERPT:
+ case TYPE_NOTE_ONLY_TITLE: {
+ ((NoteViewHolder) holder).bind(isSelected, (Note) itemList.get(position), showCategory, mainColor, textColor, searchQuery);
+ break;
+ }
+ }
+ }
+
+ public void setTracker(SelectionTracker tracker) {
+ this.tracker = tracker;
+ }
+
+ public Item getItem(int notePosition) {
+ return itemList.get(notePosition);
+ }
+
+ public boolean hasItemPosition(int notePosition) {
+ return notePosition >= 0 && notePosition < itemList.size();
+ }
+
+ public void remove(@NonNull Item item) {
+ itemList.remove(item);
+ notifyDataSetChanged();
+ }
+
+ public void setShowCategory(boolean showCategory) {
+ this.showCategory = showCategory;
+ }
+
+ @Override
+ public int getItemCount() {
+ return itemList.size();
+ }
+
+ @IntRange(from = 0, to = 3)
+ @Override
+ public int getItemViewType(int position) {
+ final var item = getItem(position);
+ if (item == null) {
+ throw new IllegalArgumentException("Item at position " + position + " must not be null");
+ }
+ if (getItem(position).isSection()) return TYPE_SECTION;
+ final var note = (Note) getItem(position);
+ if (TextUtils.isEmpty(note.getExcerpt())) {
+ if (TextUtils.isEmpty(note.getCategory())) {
+ return TYPE_NOTE_ONLY_TITLE;
+ } else {
+ return TYPE_NOTE_WITHOUT_EXCERPT;
+ }
+ }
+ return TYPE_NOTE_WITH_EXCERPT;
+ }
+
+ @Override
+ public void applyBrand(int mainColor, int textColor) {
+ this.mainColor = mainColor;
+ this.textColor = textColor;
+ notifyDataSetChanged();
+ }
+
+ public void setHighlightSearchQuery(CharSequence searchQuery) {
+ this.searchQuery = searchQuery;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * @return the position of the first {@link Item} which matches the given viewtype, -1 if not available
+ */
+ public int getFirstPositionOfViewType(@IntRange(from = 0, to = 3) int viewType) {
+ for (int i = 0; i < itemList.size(); i++) {
+ if (getItemViewType(i) == viewType) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Nullable
+ public Integer getSwipedPosition() {
+ return swipedPosition;
+ }
+
+ public void setSwipedPosition(@Nullable Integer swipedPosition) {
+ this.swipedPosition = swipedPosition;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..609d1aeff4a9191cf3c64018c1bdb41d224ee413
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/NoteViewHolder.java
@@ -0,0 +1,153 @@
+package it.niedermann.owncloud.notes.main.items;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatImageView;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.recyclerview.selection.ItemDetailsLookup;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.chip.Chip;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
+
+public abstract class NoteViewHolder extends RecyclerView.ViewHolder {
+ @NonNull
+ private final NoteClickListener noteClickListener;
+
+ public NoteViewHolder(@NonNull View v, @NonNull NoteClickListener noteClickListener) {
+ super(v);
+ this.noteClickListener = noteClickListener;
+ this.setIsRecyclable(false);
+ }
+
+ @CallSuper
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ itemView.setSelected(isSelected);
+ itemView.setOnClickListener((view) -> noteClickListener.onNoteClick(getLayoutPosition(), view));
+ }
+
+ protected void bindStatus(AppCompatImageView noteStatus, DBStatus status, int mainColor) {
+ noteStatus.setVisibility(DBStatus.VOID.equals(status) ? INVISIBLE : VISIBLE);
+ DrawableCompat.setTint(noteStatus.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(noteStatus.getContext(), mainColor));
+ }
+
+ protected void bindCategory(@NonNull Context context, @NonNull TextView noteCategory, boolean showCategory, @NonNull String category, int mainColor) {
+ final boolean isDarkThemeActive = NotesApplication.isDarkThemeActive(context);
+ noteCategory.setVisibility(showCategory && !category.isEmpty() ? View.VISIBLE : View.GONE);
+ noteCategory.setText(category);
+
+ @ColorInt final int categoryForeground;
+ @ColorInt final int categoryBackground;
+
+ if (isDarkThemeActive) {
+ if (ColorUtil.INSTANCE.isColorDark(mainColor)) {
+ if (contrastRatioIsSufficient(mainColor, Color.BLACK)) {
+ categoryBackground = mainColor;
+ categoryForeground = Color.WHITE;
+ } else {
+ categoryBackground = Color.WHITE;
+ categoryForeground = mainColor;
+ }
+ } else {
+ categoryBackground = mainColor;
+ categoryForeground = Color.BLACK;
+ }
+ } else {
+ categoryForeground = Color.BLACK;
+ if (ColorUtil.INSTANCE.isColorDark(mainColor) || contrastRatioIsSufficient(mainColor, Color.WHITE)) {
+ categoryBackground = mainColor;
+ } else {
+ categoryBackground = Color.BLACK;
+ }
+ }
+
+ noteCategory.setTextColor(categoryForeground);
+ if (noteCategory instanceof Chip) {
+ final Chip chip = (Chip) noteCategory;
+ chip.setChipStrokeColor(ColorStateList.valueOf(categoryBackground));
+ if(isDarkThemeActive) {
+ chip.setChipBackgroundColor(ColorStateList.valueOf(categoryBackground));
+ } else {
+ chip.setChipBackgroundColorResource(R.color.grid_item_background_selector);
+ }
+ } else {
+ DrawableCompat.setTint(noteCategory.getBackground(), categoryBackground);
+ }
+ }
+
+ protected void bindFavorite(@NonNull ImageView noteFavorite, boolean isFavorite) {
+ noteFavorite.setImageResource(isFavorite ? R.drawable.ic_star_yellow_24dp : R.drawable.ic_star_grey_ccc_24dp);
+ noteFavorite.setOnClickListener(view -> noteClickListener.onNoteFavoriteClick(getLayoutPosition(), view));
+ }
+
+ protected void bindSearchableContent(@NonNull Context context, @NonNull TextView textView, @Nullable CharSequence searchQuery, @NonNull String content, int mainColor) {
+ CharSequence processedContent = content;
+ if (!TextUtils.isEmpty(searchQuery)) {
+ @ColorInt final int searchBackground = ContextCompat.getColor(context, R.color.bg_highlighted);
+ @ColorInt final int searchForeground = BrandingUtil.getSecondaryForegroundColorDependingOnTheme(context, mainColor);
+
+ // The Pattern.quote method will add \Q to the very beginning of the string and \E to the end of the string
+ // It implies that the string between \Q and \E is a literal string and thus the reserved keyword in such string will be ignored.
+ // See https://stackoverflow.com/questions/15409296/what-is-the-use-of-pattern-quote-method
+ //noinspection ConstantConditions
+ final Pattern pattern = Pattern.compile("(" + Pattern.quote(searchQuery.toString()) + ")", Pattern.CASE_INSENSITIVE);
+ SpannableString spannableString = new SpannableString(content);
+ Matcher matcher = pattern.matcher(spannableString);
+
+ while (matcher.find()) {
+ spannableString.setSpan(new ForegroundColorSpan(searchForeground), matcher.start(), matcher.end(), 0);
+ spannableString.setSpan(new BackgroundColorSpan(searchBackground), matcher.start(), matcher.end(), 0);
+ }
+
+ processedContent = spannableString;
+ }
+ textView.setText(processedContent);
+ }
+
+ public abstract void showSwipe(boolean left);
+
+ @Nullable
+ public abstract View getNoteSwipeable();
+
+ public ItemDetailsLookup.ItemDetails getItemDetails() {
+ return new ItemDetailsLookup.ItemDetails() {
+ @Override
+ public int getPosition() {
+ return getAdapterPosition();
+ }
+
+ @Override
+ public Long getSelectionKey() {
+ return getItemId();
+ }
+ };
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000000000000000000000000000000000000..1f50202052cce75bd90f13ae3b4139237dee1ae2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/GridItemDecoration.java
@@ -0,0 +1,60 @@
+package it.niedermann.owncloud.notes.main.items.grid;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.StaggeredGridLayoutManager;
+
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+import it.niedermann.owncloud.notes.main.items.section.SectionItemDecoration;
+
+public class GridItemDecoration extends SectionItemDecoration {
+
+ @NonNull
+ private final ItemAdapter adapter;
+ private final int spanCount;
+ private final int gutter;
+
+ 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;
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ final int position = parent.getChildAdapterPosition(view);
+ if (position >= 0) {
+ final var lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
+
+ if (adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) {
+ lp.setFullSpan(true);
+ } else {
+ final int spanIndex = lp.getSpanIndex();
+
+ // First row gets some spacing at the top
+ final int firstSectionPosition = adapter.getFirstPositionOfViewType(ItemAdapter.TYPE_SECTION);
+ if (position < spanCount && (firstSectionPosition < 0 || position < firstSectionPosition)) {
+ outRect.top = gutter;
+ }
+
+ // First column gets some spacing at the left and the right side
+ if (spanIndex == 0) {
+ outRect.left = gutter;
+ }
+
+ // All columns get some spacing at the bottom and at the right side
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c4cecfe9b411eb59ad225c9a123e3bf52b7ce2b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
@@ -0,0 +1,57 @@
+package it.niedermann.owncloud.notes.main.items.grid;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridBinding;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.owncloud.notes.shared.util.NoteUtil.EXCERPT_LINE_SEPARATOR;
+
+public class NoteViewGridHolder extends NoteViewHolder {
+ @NonNull
+ private final ItemNotesListNoteItemGridBinding binding;
+
+ public NoteViewGridHolder(@NonNull ItemNotesListNoteItemGridBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) {
+ super(binding.getRoot(), noteClickListener);
+ this.binding = binding;
+
+ binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f);
+ binding.noteExcerpt.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * .8f);
+ if (monospace) {
+ binding.noteTitle.setTypeface(Typeface.MONOSPACE);
+ binding.noteExcerpt.setTypeface(Typeface.MONOSPACE);
+ }
+ }
+
+ public void showSwipe(boolean left) {
+ throw new UnsupportedOperationException(NoteViewGridHolder.class.getSimpleName() + " does not support swiping");
+ }
+
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
+ @NonNull final Context context = itemView.getContext();
+ bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
+ bindStatus(binding.noteStatus, note.getStatus(), mainColor);
+ bindFavorite(binding.noteFavorite, note.getFavorite());
+ bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
+ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), mainColor);
+ binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE);
+ }
+
+ @Nullable
+ public View getNoteSwipeable() {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
new file mode 100644
index 0000000000000000000000000000000000000000..416d99d2566f1a70e002757a24f472947409095c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
@@ -0,0 +1,47 @@
+package it.niedermann.owncloud.notes.main.items.grid;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.TypedValue;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemGridOnlyTitleBinding;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+public class NoteViewGridHolderOnlyTitle extends NoteViewHolder {
+ @NonNull
+ private final ItemNotesListNoteItemGridOnlyTitleBinding binding;
+
+ public NoteViewGridHolderOnlyTitle(@NonNull ItemNotesListNoteItemGridOnlyTitleBinding binding, @NonNull NoteClickListener noteClickListener, boolean monospace, @Px float fontSize) {
+ super(binding.getRoot(), noteClickListener);
+ this.binding = binding;
+
+ binding.noteTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 1.1f);
+ if (monospace) {
+ binding.noteTitle.setTypeface(Typeface.MONOSPACE);
+ }
+ }
+
+ public void showSwipe(boolean left) {
+ throw new UnsupportedOperationException(NoteViewGridHolderOnlyTitle.class.getSimpleName() + " does not support swiping");
+ }
+
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
+ @NonNull final Context context = itemView.getContext();
+ bindStatus(binding.noteStatus, note.getStatus(), mainColor);
+ bindFavorite(binding.noteFavorite, note.getFavorite());
+ bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
+ }
+
+ @Nullable
+ public View getNoteSwipeable() {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java
new file mode 100644
index 0000000000000000000000000000000000000000..c171a236a1272dc7a66ae31cd7e2719385e35c7e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithExcerpt.java
@@ -0,0 +1,47 @@
+package it.niedermann.owncloud.notes.main.items.list;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithExcerptBinding;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+public class NoteViewHolderWithExcerpt extends NoteViewHolder {
+ @NonNull
+ private final ItemNotesListNoteItemWithExcerptBinding binding;
+
+ public NoteViewHolderWithExcerpt(@NonNull ItemNotesListNoteItemWithExcerptBinding binding, @NonNull NoteClickListener noteClickListener) {
+ super(binding.getRoot(), noteClickListener);
+ this.binding = binding;
+ }
+
+ public void showSwipe(boolean left) {
+ binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE);
+ binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE);
+ binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention);
+ }
+
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
+ @NonNull final var context = itemView.getContext();
+ binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f);
+ bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
+ bindStatus(binding.noteStatus, note.getStatus(), mainColor);
+ bindFavorite(binding.noteFavorite, note.getFavorite());
+
+ bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
+ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), mainColor);
+ }
+
+ @NonNull
+ public View getNoteSwipeable() {
+ return binding.noteSwipeable;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ca136e49c5c8b9ab33cb48addefa3270f9620fc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewHolderWithoutExcerpt.java
@@ -0,0 +1,45 @@
+package it.niedermann.owncloud.notes.main.items.list;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.databinding.ItemNotesListNoteItemWithoutExcerptBinding;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+
+public class NoteViewHolderWithoutExcerpt extends NoteViewHolder {
+ @NonNull
+ private final ItemNotesListNoteItemWithoutExcerptBinding binding;
+
+ public NoteViewHolderWithoutExcerpt(@NonNull ItemNotesListNoteItemWithoutExcerptBinding binding, @NonNull NoteClickListener noteClickListener) {
+ super(binding.getRoot(), noteClickListener);
+ this.binding = binding;
+ }
+
+ public void showSwipe(boolean left) {
+ binding.noteFavoriteLeft.setVisibility(left ? View.VISIBLE : View.INVISIBLE);
+ binding.noteDeleteRight.setVisibility(left ? View.INVISIBLE : View.VISIBLE);
+ binding.noteSwipeFrame.setBackgroundResource(left ? R.color.bg_warning : R.color.bg_attention);
+ }
+
+ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, int mainColor, int textColor, @Nullable CharSequence searchQuery) {
+ super.bind(isSelected, note, showCategory, mainColor, textColor, searchQuery);
+ @NonNull final Context context = itemView.getContext();
+ binding.noteSwipeable.setAlpha(DBStatus.LOCAL_DELETED.equals(note.getStatus()) ? 0.5f : 1.0f);
+ bindCategory(context, binding.noteCategory, showCategory, note.getCategory(), mainColor);
+ bindStatus(binding.noteStatus, note.getStatus(), mainColor);
+ bindFavorite(binding.noteFavorite, note.getFavorite());
+ bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), mainColor);
+ }
+
+ @NonNull
+ public View getNoteSwipeable() {
+ return binding.noteSwipeable;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..d31bb2f30fe496c077506fce5bbefa5c5c91fde9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NotesListViewItemTouchHelper.java
@@ -0,0 +1,140 @@
+package it.niedermann.owncloud.notes.main.items.list;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.selection.SelectionTracker;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.branding.BrandedSnackbar;
+import it.niedermann.owncloud.notes.main.MainViewModel;
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+import it.niedermann.owncloud.notes.main.items.section.SectionViewHolder;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+
+public class NotesListViewItemTouchHelper extends ItemTouchHelper {
+
+ private static final String TAG = NotesListViewItemTouchHelper.class.getSimpleName();
+ private static final int UNDO_DURATION = 12_000;
+
+ public NotesListViewItemTouchHelper(
+ @NonNull Context context,
+ @NonNull MainViewModel mainViewModel,
+ @NonNull LifecycleOwner lifecycleOwner,
+ @NonNull SelectionTracker tracker,
+ @NonNull ItemAdapter adapter,
+ @NonNull SwipeRefreshLayout swipeRefreshLayout,
+ @NonNull View view,
+ boolean gridView) {
+ super(new SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
+ private boolean swipeRefreshLayoutEnabled;
+
+ @Override
+ public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
+ return false;
+ }
+
+ /**
+ * Disable swipe on sections and if grid view is enabled
+ *
+ * @param recyclerView RecyclerView
+ * @param viewHolder RecyclerView.ViewHolder
+ * @return 0 if viewHolder is section or grid view is enabled, otherwise super()
+ */
+ @Override
+ public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+ if (gridView || viewHolder instanceof SectionViewHolder) return 0;
+ return super.getSwipeDirs(recyclerView, viewHolder);
+ }
+
+ /**
+ * Delete note if note is swiped to left or right
+ *
+ * @param viewHolder RecyclerView.ViewHoler
+ * @param direction int
+ */
+ @SuppressLint("WrongConstant")
+ @Override
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+ switch (direction) {
+ case ItemTouchHelper.LEFT:
+ viewHolder.setIsRecyclable(false);
+ final var dbNoteWithoutContent = (Note) adapter.getItem(viewHolder.getLayoutPosition());
+ final var dbNoteLiveData = mainViewModel.getFullNote$(dbNoteWithoutContent.getId());
+ dbNoteLiveData.observe(lifecycleOwner, (dbNote) -> {
+ dbNoteLiveData.removeObservers(lifecycleOwner);
+ tracker.deselect(dbNote.getId());
+ final var deleteLiveData = mainViewModel.deleteNoteAndSync(dbNote.getId());
+ deleteLiveData.observe(lifecycleOwner, (next) -> deleteLiveData.removeObservers(lifecycleOwner));
+ Log.v(TAG, "Item deleted through swipe ----------------------------------------------");
+ BrandedSnackbar.make(view, context.getString(R.string.action_note_deleted, dbNote.getTitle()), UNDO_DURATION)
+ .setAction(R.string.action_undo, (View v) -> {
+ final var undoLiveData = mainViewModel.addNoteAndSync(dbNote);
+ undoLiveData.observe(lifecycleOwner, (o) -> undoLiveData.removeObservers(lifecycleOwner));
+ BrandedSnackbar.make(view, context.getString(R.string.action_note_restored, dbNote.getTitle()), Snackbar.LENGTH_SHORT)
+ .show();
+ })
+ .show();
+ });
+ break;
+ case ItemTouchHelper.RIGHT:
+ viewHolder.setIsRecyclable(false);
+ final var adapterNote = (Note) adapter.getItem(viewHolder.getLayoutPosition());
+ final var toggleLiveData = mainViewModel.toggleFavoriteAndSync(adapterNote.getId());
+ toggleLiveData.observe(lifecycleOwner, (next) -> toggleLiveData.removeObservers(lifecycleOwner));
+ break;
+ default:
+ //NoOp
+ }
+ }
+
+ @Override
+ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
+ final var noteViewHolder = (NoteViewHolder) viewHolder;
+ // show swipe icon on the side
+ noteViewHolder.showSwipe(dX > 0);
+ // move only swipeable part of item (not leave-behind)
+ getDefaultUIUtil().onDraw(c, recyclerView, noteViewHolder.getNoteSwipeable(), dX, dY, actionState, isCurrentlyActive);
+ }
+
+ @Override
+ public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
+ if (actionState == ACTION_STATE_SWIPE) {
+ Log.i(TAG, "Start swiping, disable swipeRefreshLayout");
+ swipeRefreshLayoutEnabled = swipeRefreshLayout.isEnabled();
+ swipeRefreshLayout.setEnabled(false);
+ if (viewHolder != null) {
+ adapter.setSwipedPosition(viewHolder.getLayoutPosition());
+ }
+ }
+ super.onSelectedChanged(viewHolder, actionState);
+ }
+
+ @Override
+ public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+ Log.i(TAG, "End swiping, resetting swipeRefreshLayout state");
+ swipeRefreshLayout.setEnabled(swipeRefreshLayoutEnabled);
+ getDefaultUIUtil().clearView(((NoteViewHolder) viewHolder).getNoteSwipeable());
+ adapter.setSwipedPosition(null);
+ }
+
+ @Override
+ public float getSwipeEscapeVelocity(float defaultValue) {
+ return defaultValue * 3;
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f7ca1c79583fb0302bfe40bdbd0d3eea83d5b6f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItem.java
@@ -0,0 +1,50 @@
+package it.niedermann.owncloud.notes.main.items.section;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.owncloud.notes.shared.model.Item;
+
+public class SectionItem implements Item {
+
+ private String title;
+
+ public SectionItem(String title) {
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ @Override
+ public boolean isSection() {
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SectionItem)) return false;
+
+ SectionItem that = (SectionItem) o;
+
+ return title != null ? title.equals(that.title) : that.title == null;
+ }
+
+ @Override
+ public int hashCode() {
+ return title != null ? title.hashCode() : 0;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "SectionItem{" +
+ "title='" + title + '\'' +
+ '}';
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java
new file mode 100644
index 0000000000000000000000000000000000000000..bde8f1551626f6d1a193dce02e29d13e96bee68c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionItemDecoration.java
@@ -0,0 +1,40 @@
+package it.niedermann.owncloud.notes.main.items.section;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+
+public class SectionItemDecoration extends RecyclerView.ItemDecoration {
+
+ @NonNull
+ private final ItemAdapter adapter;
+ private final int sectionLeft;
+ private final int sectionTop;
+ private final int sectionRight;
+ private final int sectionBottom;
+
+ public SectionItemDecoration(@NonNull ItemAdapter adapter, @Px int sectionLeft, @Px int sectionTop, @Px int sectionRight, @Px int sectionBottom) {
+ this.adapter = adapter;
+ this.sectionLeft = sectionLeft;
+ this.sectionTop = sectionTop;
+ this.sectionRight = sectionRight;
+ this.sectionBottom = sectionBottom;
+ }
+
+ @CallSuper
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ final int position = parent.getChildAdapterPosition(view);
+ if (position >= 0 && adapter.getItemViewType(position) == ItemAdapter.TYPE_SECTION) {
+ outRect.left = sectionLeft;
+ outRect.top = sectionTop;
+ outRect.right = sectionRight;
+ outRect.bottom = sectionBottom;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1ce4c45dfa261241b9caaef63822456352d308e
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/section/SectionViewHolder.java
@@ -0,0 +1,18 @@
+package it.niedermann.owncloud.notes.main.items.section;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.databinding.ItemNotesListSectionItemBinding;
+
+public class SectionViewHolder extends RecyclerView.ViewHolder {
+ private final ItemNotesListSectionItemBinding binding;
+
+ public SectionViewHolder(ItemNotesListSectionItemBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(SectionItem item) {
+ binding.sectionTitle.setText(item.getTitle());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..03fda02025f8c169e5fa3fcdbcb10e58a5664584
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemIdKeyProvider.java
@@ -0,0 +1,33 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemKeyProvider;
+import androidx.recyclerview.widget.RecyclerView;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+public class ItemIdKeyProvider extends ItemKeyProvider {
+ private final RecyclerView recyclerView;
+
+ public ItemIdKeyProvider(RecyclerView recyclerView) {
+ super(SCOPE_MAPPED);
+ this.recyclerView = recyclerView;
+ }
+
+ @Nullable
+ @Override
+ public Long getKey(int position) {
+ final var adapter = recyclerView.getAdapter();
+ if (adapter == null) {
+ throw new IllegalStateException("RecyclerView adapter is not set!");
+ }
+ return adapter.getItemId(position);
+ }
+
+ @Override
+ public int getPosition(@NonNull Long key) {
+ final var viewHolder = recyclerView.findViewHolderForItemId(key);
+ return viewHolder == null ? NO_POSITION : viewHolder.getLayoutPosition();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3ad40ddcc4aed659672cbb3be1e7152fc938318
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemLookup.java
@@ -0,0 +1,37 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.ItemDetailsLookup;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
+
+public class ItemLookup extends ItemDetailsLookup {
+
+ @NonNull
+ private final RecyclerView recyclerView;
+
+ public ItemLookup(@NonNull RecyclerView recyclerView) {
+ this.recyclerView = recyclerView;
+ }
+
+ @Nullable
+ @Override
+ public ItemDetails getItemDetails(@NonNull MotionEvent e) {
+ final var view = recyclerView.findChildViewUnder(e.getX(), e.getY());
+ if (view != null) {
+ final RecyclerView.ViewHolder viewHolder = recyclerView.getChildViewHolder(view);
+ if (viewHolder instanceof NoteViewHolder) {
+ return ((NoteViewHolder) recyclerView.getChildViewHolder(view))
+ .getItemDetails();
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..bce834e2dca3290139a00cbe8a6f44e10b8583ba
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/selection/ItemSelectionTracker.java
@@ -0,0 +1,46 @@
+package it.niedermann.owncloud.notes.main.items.selection;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.selection.SelectionTracker;
+import androidx.recyclerview.selection.StorageStrategy;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.owncloud.notes.main.items.ItemAdapter;
+
+public class ItemSelectionTracker {
+
+ private ItemSelectionTracker() {
+ // Use build() method
+ }
+
+ public static SelectionTracker build(@NonNull RecyclerView recyclerView, @NonNull ItemAdapter adapter) {
+ return new SelectionTracker.Builder<>(
+ ItemSelectionTracker.class.getSimpleName(),
+ recyclerView,
+ new ItemIdKeyProvider(recyclerView),
+ new ItemLookup(recyclerView),
+ StorageStrategy.createLongStorage()
+ ).withSelectionPredicate(
+ new SelectionTracker.SelectionPredicate<>() {
+ @Override
+ public boolean canSetStateForKey(@NonNull Long key, boolean nextState) {
+ return true;
+ }
+
+ @Override
+ public boolean canSetStateAtPosition(int position, boolean nextState) {
+ @Nullable Integer swipedPosition = adapter.getSwipedPosition();
+ if (!adapter.hasItemPosition(position)) {
+ return false;
+ }
+ return (swipedPosition == null || swipedPosition != position) && !adapter.getItem(position).isSection();
+ }
+
+ @Override
+ public boolean canSelectMultiple() {
+ return true;
+ }
+ }).build();
+ }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..8112f7ec35d025c1ef25a9ccbd2aae917e2574c2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/menu/MenuAdapter.java
@@ -0,0 +1,102 @@
+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.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;
+import it.niedermann.owncloud.notes.databinding.ItemNavigationBinding;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.preferences.PreferencesActivity;
+
+public class MenuAdapter extends RecyclerView.Adapter {
+
+ @NonNull
+ private final MenuItem[] menuItems;
+ @NonNull
+ private final Consumer