From 0111f23af204a5189ba59b928e9f9d9700f6bad0 Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 01:31:29 +0100 Subject: [PATCH 1/7] Add native image attachment and system image sharing to Nextcloud Notes Signed-off-by: Saboor Abdul --- app/src/main/AndroidManifest.xml | 10 ++ .../owncloud/notes/edit/BaseNoteFragment.java | 85 ++++++++- .../owncloud/notes/edit/EditNoteActivity.java | 95 ++++++++++ .../owncloud/notes/edit/NoteEditFragment.java | 14 ++ .../notes/edit/NotePreviewFragment.java | 35 ++++ .../notes/edit/NoteReadonlyFragment.java | 3 + .../notes/persistence/ApiProvider.java | 2 +- .../notes/shared/util/NoteImageHelper.java | 162 ++++++++++++++++++ .../main/res/drawable/ic_image_white_24dp.xml | 9 + app/src/main/res/menu/menu_note_fragment.xml | 6 + app/src/main/res/values/strings.xml | 3 + 11 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java create mode 100644 app/src/main/res/drawable/ic_image_white_24dp.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 111a0dc01..89d2b95d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -122,6 +122,16 @@ + + + + + + + + + + diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java index f64eec2e7..55e67e926 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java @@ -9,6 +9,7 @@ import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT; import static it.niedermann.owncloud.notes.shared.util.WidgetUtil.pendingIntentFlagCompat; +import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -21,6 +22,11 @@ import android.view.View; import android.widget.ScrollView; +import android.net.Uri; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -53,6 +59,7 @@ 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.NoteImageHelper; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.ShareUtil; @@ -83,6 +90,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego protected boolean isNew = true; + private ActivityResultLauncher pickImageLauncher; + @Override public void onAttach(@NonNull Context context) { super.onAttach(context); @@ -94,6 +103,12 @@ public void onAttach(@NonNull Context context) { repo = NotesRepository.getInstance(context); } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), this::onImagePicked); + } + @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -239,6 +254,12 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) { if (note != null) { prepareFavoriteOption(menu.findItem(R.id.menu_favorite)); + final var attachItem = menu.findItem(R.id.menu_attach_image); + if (attachItem != null) { + final var utils = BrandingUtil.of(colorAccent, requireContext()); + utils.platform.colorToolbarMenuIcon(requireContext(), attachItem); + } + 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); @@ -264,7 +285,12 @@ private void prepareFavoriteOption(MenuItem item) { @Override public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); - if (itemId == R.id.menu_cancel) { + if (itemId == R.id.menu_attach_image) { + if (pickImageLauncher != null) { + pickImageLauncher.launch("image/*"); + } + return true; + } else if (itemId == R.id.menu_cancel) { executor.submit(() -> { if (originalNote == null) { repo.deleteNoteAndSync(localAccount, note.getId()); @@ -315,6 +341,63 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + private void onImagePicked(@Nullable Uri uri) { + if (uri == null) { + return; + } + final Context context = getContext(); + if (context == null) { + return; + } + Toast.makeText(context, R.string.uploading_image, Toast.LENGTH_SHORT).show(); + executor.submit(() -> { + try { + final var ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext()); + final String attachmentPath = NoteImageHelper.uploadImage(requireContext().getApplicationContext(), ssoAccount, uri); + final String markdown = NoteImageHelper.formatMarkdownImage(attachmentPath); + final Activity activity = getActivity(); + if (activity != null && isAdded()) { + activity.runOnUiThread(() -> { + if (isAdded() && getContext() != null) { + insertImageMarkdown(markdown); + Toast.makeText(getContext(), R.string.image_attached, Toast.LENGTH_SHORT).show(); + } + }); + } + } catch (Exception e) { + Log_OC.e(TAG, "Failed to upload image attachment", e); + final Activity activity = getActivity(); + if (activity != null && isAdded()) { + activity.runOnUiThread(() -> { + if (getContext() != null) { + Toast.makeText(getContext(), "Failed to attach image: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + } + }); + } + } + }); + } + + public void insertImageMarkdown(@NonNull String markdown) { + if (this instanceof NoteEditFragment) { + ((NoteEditFragment) this).insertTextAtCursor(markdown); + saveNote(null); + } else { + if (note != null) { + String current = note.getContent(); + String updated = (current == null || current.isEmpty()) ? markdown : (current + markdown); + note = repo.updateNoteAndSync(localAccount, note, updated, null, null); + if (listener != null) { + listener.onNoteUpdated(note); + } + requireActivity().invalidateOptionsMenu(); + if (this instanceof NotePreviewFragment) { + ((NotePreviewFragment) this).onNoteLoaded(note); + } + } + } + } + private void pinNoteToHome() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; 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 index e94d30973..ba6cabea0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java @@ -12,15 +12,18 @@ import android.os.Bundle; import android.text.TextUtils; import android.util.Log; +import android.net.Uri; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; +import android.widget.ProgressBar; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; @@ -28,6 +31,8 @@ import androidx.fragment.app.Fragment; import androidx.preference.PreferenceManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; import com.nextcloud.android.sso.helper.SingleAccountHelper; @@ -36,7 +41,9 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; import java.util.Objects; import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData; @@ -50,6 +57,7 @@ import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.shared.model.NavigationCategory; +import it.niedermann.owncloud.notes.shared.util.NoteImageHelper; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.ShareUtil; @@ -343,6 +351,16 @@ private void launchNewNote() { favorite = categoryPreselection.getType() == FAVORITES; } + final String action = intent.getAction(); + final String type = intent.getType(); + final boolean isImageShare = (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) + && ((type != null && type.startsWith("image/")) || intent.hasExtra(Intent.EXTRA_STREAM)); + + if (isImageShare) { + handleImageShare(intent, categoryTitle, favorite); + return; + } + String content = ""; if ( intent.hasExtra(Intent.EXTRA_TEXT) && @@ -363,6 +381,83 @@ private void launchNewNote() { replaceFragment(); } + private void handleImageShare(Intent intent, String categoryTitle, boolean favorite) { + final List imageUris = new ArrayList<>(); + if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { + List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + if (uris != null) { + imageUris.addAll(uris); + } + } else { + Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + imageUris.add(uri); + } else if (intent.getData() != null) { + imageUris.add(intent.getData()); + } + } + + final String initialText = intent.hasExtra(Intent.EXTRA_TEXT) ? ShareUtil.extractSharedText(intent) : ""; + + if (imageUris.isEmpty()) { + final String content = initialText != null ? initialText : ""; + final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null, false, false); + fragment = getNewNoteFragment(newNote); + replaceFragment(); + return; + } + + final ProgressBar progressBar = new ProgressBar(this); + progressBar.setPadding(0, 48, 0, 48); + final AlertDialog progressDialog = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.uploading_image) + .setView(progressBar) + .setCancelable(false) + .show(); + + new Thread(() -> { + StringBuilder contentBuilder = new StringBuilder(); + if (initialText != null && !initialText.trim().isEmpty()) { + contentBuilder.append(initialText).append("\n\n"); + } + + try { + final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); + final String notesPath = NoteImageHelper.getNotesPath(getApplicationContext(), ssoAccount, repo); + + for (Uri uri : imageUris) { + try { + String attachmentPath = NoteImageHelper.uploadImage(getApplicationContext(), ssoAccount, notesPath, uri); + contentBuilder.append(NoteImageHelper.formatMarkdownImage(attachmentPath)); + } catch (Exception e) { + Log.e(TAG, "Failed to upload shared image " + uri, e); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to get SSO account or notes path for image share", e); + } + + final String finalContent = contentBuilder.toString().trim(); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + return; + } + try { + if (progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } catch (Exception ignored) { + } + + String title = NoteUtil.generateNonEmptyNoteTitle(finalContent, EditNoteActivity.this); + final var newNote = new Note(null, Calendar.getInstance(), title, finalContent, categoryTitle, favorite, null, false, false); + fragment = getNewNoteFragment(newNote); + replaceFragment(); + Toast.makeText(EditNoteActivity.this, R.string.image_attached, Toast.LENGTH_SHORT).show(); + }); + }).start(); + } + private void launchReadonlyNote() { final var intent = getIntent(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java index f0a8c334d..1e4016dd5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java @@ -223,6 +223,20 @@ protected void onNoteLoaded(Note note) { }); } + public void insertTextAtCursor(@NonNull String text) { + if (binding == null || binding.editContent == null) { + return; + } + int start = Math.max(0, binding.editContent.getSelectionStart()); + int end = Math.max(0, binding.editContent.getSelectionEnd()); + int selStart = Math.min(start, end); + int selEnd = Math.max(start, end); + if (binding.editContent.getText() != null) { + binding.editContent.getText().replace(selStart, selEnd, text, 0, text.length()); + binding.editContent.setSelection(selStart + text.length()); + } + } + private void openSoftKeyboard() { binding.editContent.postDelayed(() -> { binding.editContent.requestFocus(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java index e7bbde4d2..35ddc8ddc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java @@ -32,12 +32,14 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.nextcloud.android.sso.helper.SingleAccountHelper; +import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.owncloud.android.lib.common.utils.Log_OC; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding; import it.niedermann.owncloud.notes.persistence.entity.Note; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; import it.niedermann.owncloud.notes.shared.util.SSOUtil; import kotlin.Unit; @@ -153,7 +155,36 @@ protected void onNoteLoaded(Note note) { final String content = note.getContent(); changedText = content; + // Resolve the notes folder path from the server (e.g. "Notes") to build + // the correct WebDAV prefix for relative image references like + // "attachments/image.png" → "http://server/remote.php/webdav/Notes/attachments/image.png" + String imageUrlPrefix = ""; + try { + final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()); + final var settingsCall = repo.getServerSettings(ssoAccount, ApiVersion.API_VERSION_1_0); + final var settingsResponse = settingsCall.execute(); + final var settings = settingsResponse.body(); + final String notesPath = (settings != null && settings.getNotesPath() != null) + ? settings.getNotesPath() + : "Notes"; + // Build WebDAV prefix: serverUrl + /remote.php/webdav/ + notesPath + / + imageUrlPrefix = ssoAccount.url + "/remote.php/webdav/" + notesPath + "/"; + Log.i(TAG, "Image URL prefix set to: " + imageUrlPrefix); + } catch (Exception e) { + Log_OC.w(TAG, "Could not fetch notes path for image prefix, using server root: " + e.getMessage()); + try { + final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()); + imageUrlPrefix = ssoAccount.url + "/remote.php/webdav/Notes/"; + } catch (Exception e2) { + Log_OC.e(TAG, "Failed to get ssoAccount for image prefix: " + e2); + } + } + + final String finalImageUrlPrefix = imageUrlPrefix; onMainThread(() -> { + if (!finalImageUrlPrefix.isEmpty()) { + binding.singleNoteContent.setMarkdownImageUrlPrefix(finalImageUrlPrefix); + } binding.singleNoteContent.setMarkdownString(content, setScrollY); final var activity = getActivity(); @@ -246,6 +277,10 @@ public void applyBrand(int color) { try { final var ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getContext()); binding.singleNoteContent.setCurrentSingleSignOnAccount(ssoAccount, color); + // Enable inline image display: provide the server base URL so that relative + // image paths in Markdown (e.g. "./Photos/img.jpg") are resolved correctly, + // and absolute Nextcloud paths (e.g. "/remote.php/webdav/...") work too. + binding.singleNoteContent.setMarkdownImageUrlPrefix(ssoAccount.url); } catch (Exception e) { Log_OC.e(TAG, "applyBrand exception: " + e); } 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 index 7a1126f59..0562599b2 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java @@ -24,6 +24,9 @@ public class NoteReadonlyFragment extends NotePreviewFragment { public void onPrepareOptionsMenu(@NonNull Menu menu) { super.onPrepareOptionsMenu(menu); menu.findItem(R.id.menu_favorite).setVisible(false); + if (menu.findItem(R.id.menu_attach_image) != null) { + menu.findItem(R.id.menu_attach_image).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); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 8143a9a9e..0db1f2a75 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -121,7 +121,7 @@ public synchronized ShareAPI getShareAPI(@NonNull Context context, @NonNull Sing return shareAPI; } - private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { + public synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { if (API_CACHE.containsKey(ssoAccount.name)) { return API_CACHE.get(ssoAccount.name); } else { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java new file mode 100644 index 000000000..ab0ffcd9f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java @@ -0,0 +1,162 @@ +package it.niedermann.owncloud.notes.shared.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.nextcloud.android.sso.aidl.NextcloudRequest; +import com.nextcloud.android.sso.api.NextcloudAPI; +import com.nextcloud.android.sso.model.SingleSignOnAccount; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import it.niedermann.owncloud.notes.persistence.ApiProvider; +import it.niedermann.owncloud.notes.persistence.NotesRepository; +import it.niedermann.owncloud.notes.shared.model.ApiVersion; + +public class NoteImageHelper { + private static final String TAG = "NoteImageHelper"; + + /** + * Resolves the notes folder path on the Nextcloud server (defaults to "Notes"). + */ + @NonNull + public static String getNotesPath(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable NotesRepository repo) { + if (repo == null) { + repo = NotesRepository.getInstance(context.getApplicationContext()); + } + try { + final var call = repo.getServerSettings(ssoAccount, ApiVersion.API_VERSION_1_0); + final var response = call.execute(); + final var settings = response.body(); + if (settings != null && settings.getNotesPath() != null && !settings.getNotesPath().isEmpty()) { + return settings.getNotesPath(); + } + } catch (Exception e) { + Log.w(TAG, "Failed to query server settings for notes path, falling back to 'Notes'", e); + } + return "Notes"; + } + + /** + * Downsamples, compresses, and uploads an image to the Nextcloud WebDAV attachments folder. + * Automatically resolves the notes path. + * + * @return The relative attachment path, e.g. "attachments/abc-123.jpg" + */ + @NonNull + public static String uploadImage(@NonNull Context context, + @NonNull SingleSignOnAccount ssoAccount, + @NonNull Uri imageUri) throws Exception { + String notesPath = getNotesPath(context, ssoAccount, null); + return uploadImage(context, ssoAccount, notesPath, imageUri); + } + + /** + * Downsamples, compresses, and uploads an image to the Nextcloud WebDAV attachments folder. + * + * @return The relative attachment path, e.g. "attachments/abc-123.jpg" + */ + @NonNull + public static String uploadImage(@NonNull Context context, + @NonNull SingleSignOnAccount ssoAccount, + @NonNull String notesPath, + @NonNull Uri imageUri) throws Exception { + final ContentResolver cr = context.getContentResolver(); + + // 1. Determine format + String mimeType = cr.getType(imageUri); + boolean isPng = mimeType != null && mimeType.contains("png"); + String extension = isPng ? ".png" : ".jpg"; + String filename = UUID.randomUUID().toString() + extension; + + // 2. Safely decode bounds + BitmapFactory.Options boundsOpts = new BitmapFactory.Options(); + boundsOpts.inJustDecodeBounds = true; + try (InputStream is = cr.openInputStream(imageUri)) { + if (is == null) throw new IOException("Cannot open input stream for " + imageUri); + BitmapFactory.decodeStream(is, null, boundsOpts); + } + + // 3. Compute sample size (target max 1920px) + int sampleSize = 1; + int maxDim = 1920; + while (boundsOpts.outWidth / sampleSize / 2 >= maxDim || boundsOpts.outHeight / sampleSize / 2 >= maxDim) { + sampleSize *= 2; + } + + // 4. Decode bitmap + BitmapFactory.Options decodeOpts = new BitmapFactory.Options(); + decodeOpts.inSampleSize = sampleSize; + Bitmap bm; + try (InputStream is = cr.openInputStream(imageUri)) { + if (is == null) throw new IOException("Cannot open input stream for " + imageUri); + bm = BitmapFactory.decodeStream(is, null, decodeOpts); + } + if (bm == null) throw new IOException("Failed to decode image bitmap"); + + // 5. Scale down if still wider than maxDim + if (bm.getWidth() > maxDim) { + float ratio = (float) maxDim / bm.getWidth(); + int targetHeight = Math.round(bm.getHeight() * ratio); + bm = Bitmap.createScaledBitmap(bm, maxDim, targetHeight, true); + } + + // 6. Compress to bytes + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Bitmap.CompressFormat format = isPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; + bm.compress(format, 85, baos); + byte[] imageBytes = baos.toByteArray(); + bm.recycle(); + + // 7. Get Nextcloud SSO API + NextcloudAPI api = ApiProvider.getInstance().getNextcloudAPI(context, ssoAccount); + + // Ensure attachments directory exists (MKCOL) + String attachmentsUrl = "/remote.php/webdav/" + notesPath + "/attachments"; + try { + NextcloudRequest mkcol = new NextcloudRequest.Builder() + .setMethod("MKCOL") + .setUrl(attachmentsUrl) + .build(); + api.performNetworkRequestV2(mkcol); + Log.d(TAG, "Created attachments folder: " + attachmentsUrl); + } catch (Exception e) { + // Folder likely already exists + Log.d(TAG, "MKCOL returned (likely exists): " + e.getMessage()); + } + + // Upload the file (PUT) + String fileUrl = attachmentsUrl + "/" + filename; + NextcloudRequest put = new NextcloudRequest.Builder() + .setMethod("PUT") + .setUrl(fileUrl) + .setRequestBodyAsStream(new ByteArrayInputStream(imageBytes)) + .build(); + api.performNetworkRequestV2(put); + Log.i(TAG, "Uploaded image successfully: " + fileUrl + " (" + imageBytes.length + " bytes)"); + + return "attachments/" + filename; + } + + /** + * Formats a Markdown image tag from an attachment path. + */ + @NonNull + public static String formatMarkdownImage(@NonNull String attachmentPath) { + String filename = attachmentPath.contains("/") + ? attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1) + : attachmentPath; + return "\n\n![" + filename + "](" + attachmentPath + ")\n\n"; + } +} diff --git a/app/src/main/res/drawable/ic_image_white_24dp.xml b/app/src/main/res/drawable/ic_image_white_24dp.xml new file mode 100644 index 000000000..1b4128c53 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/menu_note_fragment.xml b/app/src/main/res/menu/menu_note_fragment.xml index e4a7bfa35..352e6f6f2 100644 --- a/app/src/main/res/menu/menu_note_fragment.xml +++ b/app/src/main/res/menu/menu_note_fragment.xml @@ -16,6 +16,12 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="ifRoom|collapseActionView" /> + Delete Category Favorite + Attach image + Uploading image… + Image attached Preview Share Shared note From 2005c6e3f185ebc80d8aa7a8d3a79d8bb949b703 Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 02:01:57 +0100 Subject: [PATCH 2/7] Show image thumbnails in note grid Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- .../owncloud/notes/NotesApplication.java | 4 +- .../main/items/grid/NoteViewGridHolder.java | 8 +- .../grid/NoteViewGridHolderOnlyTitle.java | 2 + .../main/items/list/NoteViewListHolder.java | 8 +- .../notes/persistence/dao/NoteDao.java | 18 +-- .../shared/util/NoteImagePreviewLoader.kt | 103 ++++++++++++++++++ .../layout/item_notes_list_note_item_grid.xml | 10 ++ ...m_notes_list_note_item_grid_only_title.xml | 10 ++ ...item_notes_list_note_item_with_excerpt.xml | 9 ++ app/src/main/res/values/dimens.xml | 2 + app/src/main/res/xml/preferences.xml | 1 + .../shared/util/NoteImagePreviewLoaderTest.kt | 28 +++++ 12 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt create mode 100644 app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoaderTest.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java index e82e27bb9..b68c2f0b3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java @@ -35,7 +35,7 @@ public class NotesApplication extends Application { private static boolean isLocked = true; private static long lastInteraction = 0; private static String PREF_KEY_THEME; - private static boolean isGridViewEnabled = false; + private static boolean isGridViewEnabled = true; private static boolean isSwipeEnabled = true; private static BrandingUtil brandingUtil; @@ -45,7 +45,7 @@ public void onCreate() { setAppTheme(getAppTheme(getApplicationContext())); final var prefs = getDefaultSharedPreferences(getApplicationContext()); lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false); - isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false); + isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), true); isSwipeEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_swipe_actions), true); super.onCreate(); brandingUtil = BrandingUtil.getInstance(this); 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 index 67c5c53d9..da945a2e5 100644 --- 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 @@ -25,6 +25,7 @@ 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 it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader; public class NoteViewGridHolder extends NoteViewHolder { @NonNull @@ -54,9 +55,12 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindFavorite(binding.noteFavorite, note.getFavorite()); bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); - bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color); + final boolean hasImagePreview = NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview); + if (!hasImagePreview) { + bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color); + } bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); - binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); + binding.noteExcerpt.setVisibility(hasImagePreview || TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE); } @Nullable 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 index 0699dd237..08a199c03 100644 --- 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 @@ -19,6 +19,7 @@ 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 it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader; public class NoteViewGridHolderOnlyTitle extends NoteViewHolder { @NonNull @@ -46,6 +47,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, i bindFavorite(binding.noteFavorite, note.getFavorite()); bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); + NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview); bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java index 771db1737..69cc6a9cc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java @@ -20,6 +20,7 @@ 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 it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader; public class NoteViewListHolder extends NoteViewHolder { @NonNull @@ -62,7 +63,10 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ bindModified(binding.noteModified, note.getModified()); bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color); - if (note.getExcerpt().isEmpty()) { + final boolean hasImagePreview = NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview); + if (hasImagePreview) { + binding.noteExcerpt.setVisibility(View.GONE); + } else if (note.getExcerpt().isEmpty()) { bindSearchableContent( context, binding.noteExcerpt, @@ -70,8 +74,10 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @ context.getString(R.string.listview_no_content), color ); + binding.noteExcerpt.setVisibility(View.VISIBLE); } else { bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), color); + binding.noteExcerpt.setVisibility(View.VISIBLE); } bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java index ad539dc1f..245811701 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java @@ -24,7 +24,7 @@ /** * Each method starting with search will return only a partial {@link Note} without any - * {@link Note#eTag}, {@link Note#status}, {@link Note#content} or {@link Note#scrollY} for performance reasons. + * {@link Note#eTag} or {@link Note#scrollY}. The content is included so image attachments can be previewed. */ @SuppressWarnings("JavadocReference") @Dao @@ -39,14 +39,14 @@ public interface NoteDao { String getNoteById = "SELECT * FROM NOTE WHERE id = :id"; String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId"; String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1"; - String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC"; - String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; - String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC"; - String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC"; - String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC"; - String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; - String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC"; - String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC"; + String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC"; + String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; + String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC"; + String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC"; + String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC"; + String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; + String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC"; + String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC"; @Query(getNoteById) LiveData getNoteById$(long id); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt new file mode 100644 index 000000000..922aebae3 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt @@ -0,0 +1,103 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.shared.util + +import android.content.Context +import android.net.Uri +import android.view.View +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.nextcloud.android.sso.model.SingleSignOnAccount +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors + +object NoteImagePreviewLoader { + private val notesPaths = ConcurrentHashMap() + private val executor = Executors.newSingleThreadExecutor() + + @JvmStatic + fun load(context: Context, content: String, imageView: ImageView): Boolean { + val attachmentPath = imageReference(content) ?: run { + clear(imageView) + return false + } + val account = SingleAccountHelper.getCurrentSingleSignOnAccount(context) ?: run { + clear(imageView) + return false + } + val requestKey = "${account.name}:$attachmentPath" + imageView.tag = requestKey + imageView.visibility = View.GONE + Glide.with(imageView).clear(imageView) + + executor.execute { + val notesPath = notesPaths.getOrPut(account.name) { + NoteImageHelper.getNotesPath(context.applicationContext, account, null) + } + val imageUrl = webDavUrl(account, notesPath, attachmentPath) + imageView.post { + if (imageView.tag != requestKey) return@post + imageView.visibility = View.VISIBLE + Glide.with(imageView) + .load(SingleSignOnUrl(account.name, imageUrl)) + .centerCrop() + .into(imageView) + } + } + return true + } + + @JvmStatic + fun hasImagePreview(content: String): Boolean = imageReference(content) != null + + private fun imageReference(content: String): String? { + val pathStart = content.indexOf(ATTACHMENTS_PATH) + if (pathStart < 0) { + return null + } + + var pathEnd = content.length + for (index in pathStart until content.length) { + if (content[index].isWhitespace() || content[index] == ')' || content[index] == '"' || content[index] == '\'') { + pathEnd = index + break + } + } + return content.substring(pathStart, pathEnd) + .takeIf { it.length > ATTACHMENTS_PATH.length } + } + + private fun clear(imageView: ImageView) { + imageView.tag = null + imageView.visibility = View.GONE + Glide.with(imageView).clear(imageView) + } + + private fun webDavUrl(account: SingleSignOnAccount, notesPath: String, attachmentPath: String): String = + Uri.parse(account.url).buildUpon() + .appendPath(REMOTE_PATH) + .appendPath(WEBDAV_PATH) + .appendPathSegments(notesPath) + .appendPathSegments(attachmentPath) + .build() + .toString() + + private fun Uri.Builder.appendPathSegments(path: String): Uri.Builder = + apply { + Uri.parse(path).pathSegments + .filter { it.isNotEmpty() && it != CURRENT_DIRECTORY && it != PARENT_DIRECTORY } + .forEach(::appendPath) + } + + private const val ATTACHMENTS_PATH = "attachments/" + private const val REMOTE_PATH = "remote.php" + private const val WEBDAV_PATH = "webdav" + private const val CURRENT_DIRECTORY = "." + private const val PARENT_DIRECTORY = ".." +} diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml index 2191dba0c..f76221430 100644 --- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml +++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml @@ -33,6 +33,16 @@ tools:maxLength="50" tools:text="@tools:sample/lorem/random" /> + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b42e9c41b..e1dbdea1f 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -27,6 +27,8 @@ 100dp 18dp + 56dp + 144dp 40dp 16dp diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 637abf1bc..cb4c0586c 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -69,6 +69,7 @@ android:summary="@string/settings_enable_direct_edit_summary" /> Date: Mon, 21 Sep 2026 02:11:42 +0100 Subject: [PATCH 3/7] Show notes widget as a scrolling grid Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- .../widget/notelist/NoteListWidgetFactory.kt | 31 +----------- app/src/main/res/layout/widget_entry_grid.xml | 47 +++++++++++++++++++ app/src/main/res/layout/widget_note_list.xml | 10 +++- .../xml/note_list_widget_provider_info.xml | 2 + 4 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 app/src/main/res/layout/widget_entry_grid.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt index ab5123bd7..1cff045ff 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt @@ -20,11 +20,8 @@ import com.nextcloud.android.common.ui.util.PlatformThemeUtil import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.edit.EditNoteActivity 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.persistence.entity.NotesListWidgetData -import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType -import it.niedermann.owncloud.notes.shared.model.NavigationCategory class NoteListWidgetFactory internal constructor(private val context: Context, intent: Intent) : RemoteViewsFactory { @@ -101,21 +98,6 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i } } - private fun getCreateNoteIntent(localAccount: Account): Intent { - val bundle = Bundle() - - data?.let { - val navigationCategory = if (it.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) NavigationCategory( - ENavigationCategoryType.FAVORITES - ) else NavigationCategory(localAccount.id, it.category) - - bundle.putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory) - bundle.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, it.accountId) - } - - return getEditNoteIntent(bundle) - } - private fun getOpenNoteIntent(note: Note): Intent { val bundle = Bundle().apply { putLong(EditNoteActivity.PARAM_NOTE_ID, note.id) @@ -130,20 +112,11 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i val openNoteIntent = getOpenNoteIntent(note) - var createNoteIntent: Intent? = null - data?.let { - val localAccount = repo.getAccountById(it.accountId) - createNoteIntent = getCreateNoteIntent(localAccount) - } - - return RemoteViews(context.packageName, R.layout.widget_entry).apply { + return RemoteViews(context.packageName, R.layout.widget_entry_grid).apply { setOnClickFillInIntent(R.id.widget_note_list_entry, openNoteIntent) - createNoteIntent?.let { - setOnClickFillInIntent(R.id.widget_entry_fav_icon, createNoteIntent) - } - setTextViewText(R.id.widget_entry_title, note.title) + setTextViewText(R.id.widget_entry_excerpt, note.excerpt) if (note.category.isEmpty()) { setViewVisibility(R.id.widget_entry_category, View.GONE) diff --git a/app/src/main/res/layout/widget_entry_grid.xml b/app/src/main/res/layout/widget_entry_grid.xml new file mode 100644 index 000000000..500bb61f1 --- /dev/null +++ b/app/src/main/res/layout/widget_entry_grid.xml @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_note_list.xml b/app/src/main/res/layout/widget_note_list.xml index c91ab2e55..e0cce38e1 100644 --- a/app/src/main/res/layout/widget_note_list.xml +++ b/app/src/main/res/layout/widget_note_list.xml @@ -14,14 +14,20 @@ android:paddingVertical="@dimen/widget_inner_padding_vertical" android:paddingHorizontal="@dimen/widget_inner_padding_horizontal"> - + android:verticalSpacing="@dimen/widget_note_list_outer_padding" + tools:listitem="@layout/widget_entry_grid" /> From d0fba03b72bdbc6e3dfbe1581b744ca8d46fcb36 Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 02:16:54 +0100 Subject: [PATCH 4/7] Add image previews to notes widget Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- .../shared/util/NoteImagePreviewLoader.kt | 34 ++++++++++++++++++ .../notes/widget/notelist/NoteListWidget.kt | 35 ++++++++++++++++--- .../widget/notelist/NoteListWidgetFactory.kt | 18 ++++++---- app/src/main/res/layout/widget_entry_grid.xml | 13 +++++-- app/src/main/res/layout/widget_note_list.xml | 27 +++++++++++++- app/src/main/res/values/dimens.xml | 1 + 6 files changed, 114 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt index 922aebae3..25f54a240 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt @@ -8,16 +8,20 @@ package it.niedermann.owncloud.notes.shared.util import android.content.Context import android.net.Uri +import android.graphics.Bitmap +import android.util.Log import android.view.View import android.widget.ImageView import com.bumptech.glide.Glide import com.nextcloud.android.sso.helper.SingleAccountHelper import com.nextcloud.android.sso.model.SingleSignOnAccount import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl +import java.util.concurrent.ExecutionException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors object NoteImagePreviewLoader { + private const val TAG = "NoteImagePreviewLoader" private val notesPaths = ConcurrentHashMap() private val executor = Executors.newSingleThreadExecutor() @@ -56,6 +60,36 @@ object NoteImagePreviewLoader { @JvmStatic fun hasImagePreview(content: String): Boolean = imageReference(content) != null + @JvmStatic + fun loadBitmap(context: Context, content: String, width: Int, height: Int): Bitmap? { + val attachmentPath = imageReference(content) ?: return null + val account = SingleAccountHelper.getCurrentSingleSignOnAccount(context) ?: return null + val notesPath = try { + notesPaths.getOrPut(account.name) { + NoteImageHelper.getNotesPath(context.applicationContext, account, null) + } + } catch (exception: Exception) { + Log.e(TAG, "Unable to resolve notes path for widget image preview", exception) + return null + } + + return try { + Glide.with(context.applicationContext) + .asBitmap() + .load(SingleSignOnUrl(account.name, webDavUrl(account, notesPath, attachmentPath))) + .centerCrop() + .submit(width, height) + .get() + } catch (exception: InterruptedException) { + Thread.currentThread().interrupt() + Log.w(TAG, "Widget image preview loading was interrupted", exception) + null + } catch (exception: ExecutionException) { + Log.w(TAG, "Unable to load widget image preview", exception) + null + } + } + private fun imageReference(content: String): String? { val pathStart = content.indexOf(ATTACHMENTS_PATH) if (pathStart < 0) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt index fd15fa597..a87d171bf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt @@ -13,13 +13,16 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.util.Log import android.widget.RemoteViews import com.owncloud.android.lib.common.utils.Log_OC import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.edit.EditNoteActivity import it.niedermann.owncloud.notes.persistence.NotesRepository -import it.niedermann.owncloud.notes.shared.util.WidgetUtil +import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData +import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType +import it.niedermann.owncloud.notes.shared.model.NavigationCategory import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import androidx.core.net.toUri @@ -97,14 +100,21 @@ class NoteListWidget : AppWidgetProvider() { setPackage(context.packageName) } - val pendingIntentFlags = - WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT or Intent.FILL_IN_COMPONENT) + val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 val editNotePendingIntent = - PendingIntent.getActivity(context, 0, editNoteIntent, pendingIntentFlags) + PendingIntent.getActivity(context, appWidgetId, editNoteIntent, pendingIntentFlags) + val createNotePendingIntent = PendingIntent.getActivity( + context, + appWidgetId, + getCreateNoteIntent(context, data), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) val views = RemoteViews(context.packageName, R.layout.widget_note_list).apply { setRemoteAdapter(R.id.note_list_widget_lv, serviceIntent) setPendingIntentTemplate(R.id.note_list_widget_lv, editNotePendingIntent) + setOnClickPendingIntent(R.id.widget_add_note, createNotePendingIntent) setEmptyView( R.id.note_list_widget_lv, R.id.widget_note_list_placeholder_tv @@ -126,5 +136,22 @@ class NoteListWidget : AppWidgetProvider() { } context.sendBroadcast(intent) } + + private fun getCreateNoteIntent( + context: Context, + data: NotesListWidgetData + ): Intent { + val navigationCategory = if (data.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) { + NavigationCategory(ENavigationCategoryType.FAVORITES) + } else { + NavigationCategory(data.accountId, data.category) + } + + return Intent(context, EditNoteActivity::class.java).apply { + setPackage(context.packageName) + putExtra(EditNoteActivity.PARAM_CATEGORY, navigationCategory) + putExtra(EditNoteActivity.PARAM_ACCOUNT_ID, data.accountId) + } + } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt index 1cff045ff..7a90fb9b7 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt @@ -22,6 +22,7 @@ import it.niedermann.owncloud.notes.edit.EditNoteActivity import it.niedermann.owncloud.notes.persistence.NotesRepository import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData +import it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader class NoteListWidgetFactory internal constructor(private val context: Context, intent: Intent) : RemoteViewsFactory { @@ -116,7 +117,16 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i setOnClickFillInIntent(R.id.widget_note_list_entry, openNoteIntent) setTextViewText(R.id.widget_entry_title, note.title) - setTextViewText(R.id.widget_entry_excerpt, note.excerpt) + setTextViewText(R.id.widget_entry_excerpt, note.excerpt.replace("\uFFFC", "").trim()) + val thumbnailSize = context.resources.getDimensionPixelSize( + R.dimen.widget_note_list_thumbnail_size + ) + NoteImagePreviewLoader.loadBitmap(context, note.content, thumbnailSize, thumbnailSize) + ?.let { image -> + setImageViewBitmap(R.id.widget_entry_image_preview, image) + setViewVisibility(R.id.widget_entry_image_preview, View.VISIBLE) + } + ?: setViewVisibility(R.id.widget_entry_image_preview, View.GONE) if (note.category.isEmpty()) { setViewVisibility(R.id.widget_entry_category, View.GONE) @@ -133,12 +143,6 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i setTextColor(R.id.widget_entry_category, textColor) } - val starIconId = if (note.favorite) { - R.drawable.ic_star_yellow_24dp - } else { - R.drawable.ic_star_grey_ccc_24dp - } - setImageViewResource(R.id.widget_entry_fav_icon, starIconId) } } diff --git a/app/src/main/res/layout/widget_entry_grid.xml b/app/src/main/res/layout/widget_entry_grid.xml index 500bb61f1..07fa20ef0 100644 --- a/app/src/main/res/layout/widget_entry_grid.xml +++ b/app/src/main/res/layout/widget_entry_grid.xml @@ -8,18 +8,27 @@ + + diff --git a/app/src/main/res/layout/widget_note_list.xml b/app/src/main/res/layout/widget_note_list.xml index e0cce38e1..4cb33acce 100644 --- a/app/src/main/res/layout/widget_note_list.xml +++ b/app/src/main/res/layout/widget_note_list.xml @@ -14,10 +14,35 @@ android:paddingVertical="@dimen/widget_inner_padding_vertical" android:paddingHorizontal="@dimen/widget_inner_padding_horizontal"> + + + + + + + 4dp 26dp 20dp + 40dp 48dp From 9b07ccdcafc72163cd212baf2278a04358c0a785 Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 02:19:39 +0100 Subject: [PATCH 5/7] Size notes widget cards by content Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- app/src/main/res/layout/widget_entry_grid.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/widget_entry_grid.xml b/app/src/main/res/layout/widget_entry_grid.xml index 07fa20ef0..f7b2a1383 100644 --- a/app/src/main/res/layout/widget_entry_grid.xml +++ b/app/src/main/res/layout/widget_entry_grid.xml @@ -8,9 +8,9 @@ @@ -28,7 +28,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:layout_marginTop="@dimen/spacer_1hx" - android:maxLines="1" + android:maxLines="3" android:textColor="@color/widget_foreground" android:textSize="15sp" android:textStyle="bold" /> @@ -36,11 +36,10 @@ From 72fec38ff176a6881ac6b76529fe25a5fcaf786e Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 02:24:41 +0100 Subject: [PATCH 6/7] Match notes widget card proportions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- app/src/main/res/layout/widget_entry_grid.xml | 2 +- app/src/main/res/layout/widget_note_list.xml | 41 ++++++------------- app/src/main/res/values/dimens.xml | 2 +- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/app/src/main/res/layout/widget_entry_grid.xml b/app/src/main/res/layout/widget_entry_grid.xml index f7b2a1383..a3262b1cc 100644 --- a/app/src/main/res/layout/widget_entry_grid.xml +++ b/app/src/main/res/layout/widget_entry_grid.xml @@ -17,7 +17,7 @@ diff --git a/app/src/main/res/layout/widget_note_list.xml b/app/src/main/res/layout/widget_note_list.xml index 4cb33acce..45999b3a0 100644 --- a/app/src/main/res/layout/widget_note_list.xml +++ b/app/src/main/res/layout/widget_note_list.xml @@ -5,44 +5,18 @@ ~ SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors ~ SPDX-License-Identifier: GPL-3.0-or-later --> - - - - - - - - + + - + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3dfd430be..4e6351153 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -78,7 +78,7 @@ 4dp 26dp 20dp - 40dp + 56dp 48dp From d6fcc3a1855efbbb0e9ae03e8c0721cddf2d8920 Mon Sep 17 00:00:00 2001 From: Saboor Abdul Date: Mon, 21 Sep 2026 02:31:57 +0100 Subject: [PATCH 7/7] Limit image preview query data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Saboor Abdul --- .../notes/persistence/dao/NoteDao.java | 20 ++++++++++--------- .../notes/shared/util/NoteImageHelper.java | 10 ++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java index 245811701..a4373936c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java @@ -24,7 +24,8 @@ /** * Each method starting with search will return only a partial {@link Note} without any - * {@link Note#eTag} or {@link Note#scrollY}. The content is included so image attachments can be previewed. + * {@link Note#eTag} or {@link Note#scrollY}. The content contains only the bounded attachment-path fragment + * needed for image previews. */ @SuppressWarnings("JavadocReference") @Dao @@ -39,14 +40,15 @@ public interface NoteDao { String getNoteById = "SELECT * FROM NOTE WHERE id = :id"; String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId"; String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1"; - String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC"; - String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; - String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC"; - String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC"; - String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC"; - String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; - String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC"; - String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC"; + String imagePreviewPath = "CASE WHEN instr(content, 'attachments/') > 0 THEN substr(content, instr(content, 'attachments/'), 512) ELSE '' END as content"; + String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC"; + String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; + String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC"; + String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC"; + String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC"; + String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC"; + String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC"; + String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC"; @Query(getNoteById) LiveData getNoteById$(long id); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java index ab0ffcd9f..0e4ba7b43 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java @@ -105,11 +105,13 @@ public static String uploadImage(@NonNull Context context, } if (bm == null) throw new IOException("Failed to decode image bitmap"); - // 5. Scale down if still wider than maxDim - if (bm.getWidth() > maxDim) { - float ratio = (float) maxDim / bm.getWidth(); + // 5. Scale down if either dimension still exceeds maxDim + int largestDimension = Math.max(bm.getWidth(), bm.getHeight()); + if (largestDimension > maxDim) { + float ratio = (float) maxDim / largestDimension; + int targetWidth = Math.round(bm.getWidth() * ratio); int targetHeight = Math.round(bm.getHeight() * ratio); - bm = Bitmap.createScaledBitmap(bm, maxDim, targetHeight, true); + bm = Bitmap.createScaledBitmap(bm, targetWidth, targetHeight, true); } // 6. Compress to bytes