diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index 580e5d2e19..7c3677ecb9 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -137,7 +137,7 @@ public class AdapterAttachment extends RecyclerView.Adapter() { @Override protected Void onExecute(Context context, Bundle args) { long id = args.getLong("id"); - boolean readonly = args.getBoolean("readonly"); EntityAttachment attachment; @@ -362,21 +360,14 @@ public class AdapterAttachment extends RecyclerView.Adapter attachments = db.attachment().getAttachments(message.id); for (EntityAttachment attachment : attachments) - if (attachment.id.equals(id) && BuildConfig.DEBUG) { + if (ids.contains(attachment.id)) { Part apart = aparts.get(attachment.sequence - 1).part; if (!deletePart(icopy, apart)) throw new IllegalArgumentException("Attachment part not found"); @@ -2119,16 +2124,19 @@ class Core { ifolder.appendMessages(new Message[]{icopy}); - imessage.setFlag(Flags.Flag.DELETED, true); - expunge(context, ifolder, Arrays.asList(imessage)); + if (trash == null) { + imessage.setFlag(Flags.Flag.DELETED, true); + expunge(context, ifolder, Arrays.asList(imessage)); + } else { + EntityOperation.queue(context, message, EntityOperation.MOVE, trash.id); + } } static boolean deletePart(Part part, Part attachment) throws MessagingException, IOException { boolean deleted = false; if (part.isMimeType("multipart/*")) { Multipart multipart = (Multipart) part.getContent(); - int count = multipart.getCount(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < multipart.getCount(); i++) { Part child = multipart.getBodyPart(i); if (child == attachment) { multipart.removeBodyPart(i); diff --git a/app/src/main/java/eu/faircode/email/EntityAttachment.java b/app/src/main/java/eu/faircode/email/EntityAttachment.java index 71fa7615b9..889003e021 100644 --- a/app/src/main/java/eu/faircode/email/EntityAttachment.java +++ b/app/src/main/java/eu/faircode/email/EntityAttachment.java @@ -31,6 +31,7 @@ import androidx.core.content.FileProvider; import androidx.preference.PreferenceManager; import androidx.room.Entity; import androidx.room.ForeignKey; +import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; @@ -98,6 +99,9 @@ public class EntityAttachment { public String media_uri; public String error; + @Ignore + public boolean selected = true; + // Gmail sends inline images as attachments with a name and cid boolean isInline() { diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index ca9ca1bac2..1bb0508ee1 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -937,6 +937,9 @@ public class EntityOperation { Log.e(ex); } + if (DETACH.equals(name) && message != null) + db.message().setMessageUiHide(message, false); + if (SYNC.equals(name)) db.folder().setFolderSyncState(folder, null); diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogDeleteAttachments.java b/app/src/main/java/eu/faircode/email/FragmentDialogDeleteAttachments.java new file mode 100644 index 0000000000..d81643d4aa --- /dev/null +++ b/app/src/main/java/eu/faircode/email/FragmentDialogDeleteAttachments.java @@ -0,0 +1,273 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail 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. + + FairEmail 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 FairEmail. If not, see . + + Copyright 2018-2023 by Marcel Bokhorst (M66B) +*/ + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.json.JSONArray; + +import java.util.ArrayList; +import java.util.List; + +public class FragmentDialogDeleteAttachments extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final Context context = getContext(); + + final View dview = LayoutInflater.from(context).inflate(R.layout.dialog_delete_attachments, null); + final RecyclerView rvAttachment = dview.findViewById(R.id.rvAttachment); + final ProgressBar pbWait = dview.findViewById(R.id.pbWait); + + rvAttachment.setHasFixedSize(false); + rvAttachment.setLayoutManager(new LinearLayoutManager(context)); + + AdapterAttachmentSelect adapter = new AdapterAttachmentSelect(context); + rvAttachment.setAdapter(adapter); + + new SimpleTask>() { + @Override + protected void onPreExecute(Bundle args) { + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + pbWait.setVisibility(View.GONE); + } + + @Override + protected List onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + return db.attachment().getAttachments(id); + } + + @Override + protected void onExecuted(final Bundle args, List attachments) { + if (attachments == null) + attachments = new ArrayList<>(); + adapter.set(attachments); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, getArguments(), "attachments:delete"); + + return new AlertDialog.Builder(context) + .setIcon(R.drawable.twotone_attachment_24) + .setTitle(R.string.title_delete_attachments) + .setView(dview) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Bundle args = new Bundle(); + args.putLong("id", getArguments().getLong("id")); + args.putLongArray("ids", Helper.toLongArray(adapter.getSelectedItems())); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + long[] ids = args.getLongArray("ids"); + + DB db = DB.getInstance(context); + EntityMessage message = db.message().getMessage(id); + if (message == null) + return null; + + JSONArray jids = new JSONArray(); + for (int i = 0; i < ids.length; i++) + jids.put(i, ids[i]); + + EntityOperation.queue(context, message, EntityOperation.DETACH, jids); + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(FragmentDialogDeleteAttachments.this, args, "delete:attachments"); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .create(); + } + + public static class AdapterAttachmentSelect extends RecyclerView.Adapter { + private Context context; + private LayoutInflater inflater; + + private List items = new ArrayList<>(); + + public class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener { + private CheckBox cbEnabled; + private TextView tvInfo; + + ViewHolder(View itemView) { + super(itemView); + cbEnabled = itemView.findViewById(R.id.cbEnabled); + tvInfo = itemView.findViewById(R.id.tvInfo); + } + + private void wire() { + cbEnabled.setOnCheckedChangeListener(this); + } + + private void unwire() { + cbEnabled.setOnCheckedChangeListener(null); + } + + private void bindTo(EntityAttachment attachment) { + cbEnabled.setText(attachment.name); + cbEnabled.setChecked(attachment.selected); + + StringBuilder sb = new StringBuilder(); + if (!TextUtils.isEmpty(attachment.type)) + sb.append(attachment.type); + if (attachment.size != null) { + if (sb.length() > 0) + sb.append(' '); + sb.append(Helper.humanReadableByteCount(attachment.size)); + } + tvInfo.setText(sb.toString()); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + int pos = getAdapterPosition(); + if (pos == RecyclerView.NO_POSITION) + return; + + items.get(pos).selected = isChecked; + } + } + + AdapterAttachmentSelect(Context context) { + this.context = context; + this.inflater = LayoutInflater.from(context); + + setHasStableIds(true); + } + + public void set(List attachments) { + Log.i("Set attachments=" + attachments.size()); + + DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, attachments), false); + items = attachments; + + try { + diff.dispatchUpdatesTo(this); + } catch (Throwable ex) { + Log.e(ex); + } + } + + List getSelectedItems() { + List result = new ArrayList<>(); + for (EntityAttachment attachment : items) + if (attachment.selected) + result.add(attachment.id); + return result; + } + + private class DiffCallback extends DiffUtil.Callback { + private List prev = new ArrayList<>(); + private List next = new ArrayList<>(); + + DiffCallback(List prev, List next) { + this.prev.addAll(prev); + this.next.addAll(next); + } + + @Override + public int getOldListSize() { + return prev.size(); + } + + @Override + public int getNewListSize() { + return next.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + EntityAttachment a1 = prev.get(oldItemPosition); + EntityAttachment a2 = next.get(newItemPosition); + return a1.id.equals(a2.id); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + EntityAttachment a1 = prev.get(oldItemPosition); + EntityAttachment a2 = next.get(newItemPosition); + return a1.equals(a2); + } + } + + @Override + public long getItemId(int position) { + return items.get(position).id; + } + + @Override + public int getItemCount() { + return items.size(); + } + + @Override + @NonNull + public AdapterAttachmentSelect.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new AdapterAttachmentSelect.ViewHolder(inflater.inflate(R.layout.item_attachment_delete, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull AdapterAttachmentSelect.ViewHolder holder, int position) { + holder.unwire(); + EntityAttachment attachment = items.get(position); + holder.bindTo(attachment); + holder.wire(); + } + } +} diff --git a/app/src/main/res/layout/dialog_delete_attachments.xml b/app/src/main/res/layout/dialog_delete_attachments.xml new file mode 100644 index 0000000000..7ad3d8c968 --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_attachments.xml @@ -0,0 +1,44 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_attachment_delete.xml b/app/src/main/res/layout/item_attachment_delete.xml new file mode 100644 index 0000000000..58c0ffff0a --- /dev/null +++ b/app/src/main/res/layout/item_attachment_delete.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/popup_message_more.xml b/app/src/main/res/menu/popup_message_more.xml index ce98245174..0d22d91497 100644 --- a/app/src/main/res/menu/popup_message_more.xml +++ b/app/src/main/res/menu/popup_message_more.xml @@ -54,6 +54,11 @@ android:icon="@drawable/twotone_file_copy_24" android:title="@string/title_copy_to" /> + + Move to %1$s … Treat as spam Delete permanently + Delete attachments Snooze … Archive Reply