diff --git a/app/schemas/eu.faircode.email.DB/1.json b/app/schemas/eu.faircode.email.DB/1.json index 9d99db6f..20271fdf 100644 --- a/app/schemas/eu.faircode.email.DB/1.json +++ b/app/schemas/eu.faircode.email.DB/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "984985d3928b4abdec0802b65e820b2b", + "identityHash": "0b5e9888b548ea410934b7082b08a3b6", "entities": [ { "tableName": "identity", @@ -569,7 +569,7 @@ }, { "tableName": "attachment", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `content` BLOB, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `filename` TEXT, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -614,9 +614,9 @@ "notNull": false }, { - "fieldPath": "content", - "columnName": "content", - "affinity": "BLOB", + "fieldPath": "filename", + "columnName": "filename", + "affinity": "TEXT", "notNull": false } ], @@ -752,7 +752,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"984985d3928b4abdec0802b65e820b2b\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"0b5e9888b548ea410934b7082b08a3b6\")" ] } } \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index 4bf21657..8599ad9f 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -25,7 +25,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; -import android.text.TextUtils; +import android.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -36,7 +36,6 @@ import android.widget.TextView; import android.widget.Toast; import java.io.File; -import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -52,15 +51,18 @@ import androidx.recyclerview.widget.RecyclerView; public class AdapterAttachment extends RecyclerView.Adapter { private Context context; private LifecycleOwner owner; + private boolean debug; - private List all = new ArrayList<>(); - private List filtered = new ArrayList<>(); + private List all = new ArrayList<>(); + private List filtered = new ArrayList<>(); public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { View itemView; TextView tvName; TextView tvSize; ImageView ivStatus; + TextView tvType; + TextView tvFile; ProgressBar progressbar; ViewHolder(View itemView) { @@ -70,6 +72,8 @@ public class AdapterAttachment extends RecyclerView.Adapter() { - @Override - protected Void onLoad(Context context, Bundle args) throws Throwable { - long id = args.getLong("id"); - File file = (File) args.getSerializable("file"); - File dir = (File) args.getSerializable("dir"); - - // Create file - if (!file.exists()) { - dir.mkdir(); - file.createNewFile(); - - // Get attachment content - byte[] content = DB.getInstance(context).attachment().getContent(id); - - // Write attachment content to file - FileOutputStream fos = null; - try { - fos = new FileOutputStream(file); - fos.write(content); - } finally { - if (fos != null) - fos.close(); - } - } - - return null; - } - - @Override - protected void onLoaded(Bundle args, Void data) { - context.startActivity(intent); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - Toast.makeText(context, ex.toString(), Toast.LENGTH_LONG).show(); - } - }.load(context, owner, args); - } else { + if (attachment.filename == null) { if (attachment.progress == null) { Bundle args = new Bundle(); args.putLong("id", attachment.id); @@ -219,6 +155,33 @@ public class AdapterAttachment extends RecyclerView.Adapter attachments) { + public void set(@NonNull List attachments) { Log.i(Helper.TAG, "Set attachments=" + attachments.size()); - Collections.sort(attachments, new Comparator() { + Collections.sort(attachments, new Comparator() { @Override - public int compare(TupleAttachment a1, TupleAttachment a2) { + public int compare(EntityAttachment a1, EntityAttachment a2) { return a1.sequence.compareTo(a2.sequence); } }); @@ -272,10 +236,10 @@ public class AdapterAttachment extends RecyclerView.Adapter prev; - private List next; + private List prev; + private List next; - MessageDiffCallback(List prev, List next) { + MessageDiffCallback(List prev, List next) { this.prev = prev; this.next = next; } @@ -292,15 +256,15 @@ public class AdapterAttachment extends RecyclerView.Adapter> liveAttachments(long id); + LiveData> liveAttachments(long id); - @Query("SELECT attachment.id, attachment.message, sequence, name, type, size, progress" + - ", (NOT content IS NULL) as content" + - " FROM attachment" + + @Query("SELECT attachment.* FROM attachment" + " JOIN message ON message.id = attachment.message" + " WHERE folder = :folder" + " AND msgid = :msgid" + " ORDER BY sequence") - LiveData> liveAttachments(long folder, String msgid); - - @Query("SELECT * FROM attachment" + - " WHERE message = :message" + - " AND sequence = :sequence") - EntityAttachment getAttachment(long message, int sequence); + LiveData> liveAttachments(long folder, String msgid); @Query("SELECT COUNT(attachment.id)" + " FROM attachment" + " WHERE message = :message") int getAttachmentCount(long message); - @Query("SELECT SUM(CASE WHEN content IS NULL THEN 1 ELSE 0 END)" + - " FROM attachment" + - " WHERE message = :message") - int getAttachmentCountWithoutContent(long message); - - @Query("SELECT id, message, sequence, name, type, size, progress, NULL AS content FROM attachment" + + @Query("SELECT * FROM attachment" + " WHERE message = :message" + " ORDER BY sequence") List getAttachments(long message); + @Query("SELECT * FROM attachment" + + " WHERE message = :message" + + " AND sequence = :sequence") + EntityAttachment getAttachment(long message, int sequence); + @Query("UPDATE attachment SET progress = :progress WHERE id = :id") void setProgress(long id, int progress); - @Query("SELECT content FROM attachment WHERE id = :id") - byte[] getContent(long id); - @Insert long insertAttachment(EntityAttachment attachment); diff --git a/app/src/main/java/eu/faircode/email/EntityAttachment.java b/app/src/main/java/eu/faircode/email/EntityAttachment.java index 4c003e71..2e93015c 100644 --- a/app/src/main/java/eu/faircode/email/EntityAttachment.java +++ b/app/src/main/java/eu/faircode/email/EntityAttachment.java @@ -54,7 +54,7 @@ public class EntityAttachment { public String type; public Integer size; public Integer progress; - public byte[] content; + public String filename; @Ignore BodyPart part; @@ -69,7 +69,7 @@ public class EntityAttachment { this.type.equals(other.type) && (this.size == null ? other.size == null : this.size.equals(other.size)) && (this.progress == null ? other.progress == null : this.progress.equals(other.progress)) && - (this.content == null ? other.content == null : other.content != null)); + (this.filename == null ? other.filename == null : this.filename.equals(other.filename))); } else return false; } diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index c847fee2..31bcc357 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -50,9 +50,12 @@ import android.widget.Toast; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.snackbar.Snackbar; -import java.io.ByteArrayOutputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -412,37 +415,58 @@ public class FragmentCompose extends FragmentEx { attachment.progress = 0; attachment.id = db.attachment().insertAttachment(attachment); - Log.i(Helper.TAG, "Created attachment seq=" + attachment.sequence + " name=" + attachment.name); + Log.i(Helper.TAG, "Created attachment seq=" + attachment.sequence + + " name=" + attachment.name + " type=" + attachment.type); db.setTransactionSuccessful(); } finally { db.endTransaction(); } - InputStream is = null; try { - is = context.getContentResolver().openInputStream(uri); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - - int len; - byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; - while ((len = is.read(buffer)) > 0) { - os.write(buffer, 0, len); - - // Update progress - if (attachment.size != null) { - attachment.progress = os.size() * 100 / attachment.size; - db.attachment().updateAttachment(attachment); + File dir = new File(context.getFilesDir(), "attachments"); + dir.mkdir(); + File file = new File(dir, Long.toString(attachment.id)); + + InputStream is = null; + OutputStream os = null; + try { + is = context.getContentResolver().openInputStream(uri); + os = new BufferedOutputStream(new FileOutputStream(file)); + + int size = 0; + byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; + for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { + size += len; + os.write(buffer, 0, len); + + // Update progress + if (attachment.size != null) { + attachment.progress = size * 100 / attachment.size; + db.attachment().updateAttachment(attachment); + } + } + + attachment.size = size; + attachment.progress = null; + attachment.filename = file.getName(); + } finally { + try { + if (is != null) + is.close(); + } finally { + if (os != null) + os.close(); } } - attachment.size = os.size(); + db.attachment().updateAttachment(attachment); + } catch (Throwable ex) { + // Reset progress on failure attachment.progress = null; - attachment.content = os.toByteArray(); + attachment.filename = null; db.attachment().updateAttachment(attachment); - } finally { - if (is != null) - is.close(); + throw ex; } return null; @@ -594,9 +618,9 @@ public class FragmentCompose extends FragmentEx { DB db = DB.getInstance(getContext()); db.attachment().liveAttachments(draft.folder, draft.msgid).observe(getViewLifecycleOwner(), - new Observer>() { + new Observer>() { @Override - public void onChanged(@Nullable List attachments) { + public void onChanged(@Nullable List attachments) { if (attachments != null) adapter.set(attachments); grpAttachments.setVisibility(attachments != null && attachments.size() > 0 ? View.VISIBLE : View.GONE); @@ -794,8 +818,6 @@ public class FragmentCompose extends FragmentEx { // Save attachments List attachments = db.attachment().getAttachments(draft.id); - for (EntityAttachment attachment : attachments) - attachment.content = db.attachment().getContent(attachment.id); // Delete previous draft draft.msgid = null; @@ -829,16 +851,14 @@ public class FragmentCompose extends FragmentEx { if (draft.to == null && draft.cc == null && draft.bcc == null) throw new IllegalArgumentException(context.getString(R.string.title_to_missing)); - if (db.attachment().getAttachmentCountWithoutContent(draft.id) > 0) - throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing)); - // Save message ID String msgid = draft.msgid; // Save attachments List attachments = db.attachment().getAttachments(draft.id); for (EntityAttachment attachment : attachments) - attachment.content = db.attachment().getContent(attachment.id); + if (attachment.filename == null) + throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing)); // Delete draft (cannot move to outbox) draft.msgid = null; diff --git a/app/src/main/java/eu/faircode/email/FragmentMessage.java b/app/src/main/java/eu/faircode/email/FragmentMessage.java index a2d68c79..54253bf5 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessage.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessage.java @@ -342,9 +342,9 @@ public class FragmentMessage extends FragmentEx { // Observe attachments db.attachment().liveAttachments(id).observe(getViewLifecycleOwner(), - new Observer>() { + new Observer>() { @Override - public void onChanged(@Nullable List attachments) { + public void onChanged(@Nullable List attachments) { if (attachments != null) adapter.set(attachments); grpAttachments.setVisibility(attachments != null && attachments.size() > 0 ? View.VISIBLE : View.GONE); diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 643ffce6..be110459 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -19,12 +19,14 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import android.content.Context; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; @@ -34,7 +36,8 @@ import java.util.List; import java.util.Properties; import javax.activation.DataHandler; -import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.activation.FileTypeMap; import javax.mail.Address; import javax.mail.BodyPart; import javax.mail.Flags; @@ -48,7 +51,6 @@ import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; -import javax.mail.util.ByteArrayDataSource; public class MessageHelper { private MimeMessage imessage; @@ -84,7 +86,7 @@ public class MessageHelper { return props; } - static MimeMessageEx from(EntityMessage message, List attachments, Session isession) throws MessagingException { + static MimeMessageEx from(Context context, EntityMessage message, List attachments, Session isession) throws MessagingException { MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid); imessage.setFlag(Flags.Flag.SEEN, message.seen); @@ -118,15 +120,29 @@ public class MessageHelper { bpMessage.setContent(message.body, "text/html; charset=" + Charset.defaultCharset().name()); multipart.addBodyPart(bpMessage); - for (EntityAttachment attachment : attachments) { - BodyPart bpAttachment = new MimeBodyPart(); - bpAttachment.setFileName(attachment.name); - - DataSource dataSource = new ByteArrayDataSource(attachment.content, attachment.type); - bpAttachment.setDataHandler(new DataHandler(dataSource)); - - multipart.addBodyPart(bpAttachment); - } + for (final EntityAttachment attachment : attachments) + if (attachment.filename != null) { + BodyPart bpAttachment = new MimeBodyPart(); + bpAttachment.setFileName(attachment.name); + + File dir = new File(context.getFilesDir(), "attachments"); + File file = new File(dir, attachment.filename); + FileDataSource dataSource = new FileDataSource(file); + dataSource.setFileTypeMap(new FileTypeMap() { + @Override + public String getContentType(File file) { + return attachment.type; + } + + @Override + public String getContentType(String filename) { + return attachment.type; + } + }); + bpAttachment.setDataHandler(new DataHandler(dataSource)); + + multipart.addBodyPart(bpAttachment); + } imessage.setContent(multipart); } @@ -136,8 +152,8 @@ public class MessageHelper { return imessage; } - static MimeMessageEx from(EntityMessage message, EntityMessage reply, List attachments, Session isession) throws MessagingException { - MimeMessageEx imessage = from(message, attachments, isession); + static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List attachments, Session isession) throws MessagingException { + MimeMessageEx imessage = from(context, message, attachments, isession); imessage.addHeader("In-Reply-To", reply.msgid); imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid); return imessage; diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 0c2af2a9..a43bef28 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -49,9 +49,12 @@ import com.sun.mail.smtp.SMTPSendFailedException; import org.json.JSONArray; import org.json.JSONException; -import java.io.ByteArrayOutputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.Calendar; @@ -844,12 +847,10 @@ public class ServiceSynchronize extends LifecycleService { // Append message List attachments = db.attachment().getAttachments(message.id); - for (EntityAttachment attachment : attachments) - attachment.content = db.attachment().getContent(attachment.id); Properties props = MessageHelper.getSessionProperties(); Session isession = Session.getInstance(props, null); - MimeMessage imessage = MessageHelper.from(message, attachments, isession); + MimeMessage imessage = MessageHelper.from(this, message, attachments, isession); ifolder.appendMessages(new Message[]{imessage}); } @@ -875,15 +876,13 @@ public class ServiceSynchronize extends LifecycleService { Log.w(Helper.TAG, "MOVE by DELETE/APPEND"); List attachments = db.attachment().getAttachments(message.id); - for (EntityAttachment attachment : attachments) - attachment.content = db.attachment().getContent(attachment.id); if (!EntityFolder.ARCHIVE.equals(folder.type)) { imessage.setFlag(Flags.Flag.DELETED, true); ifolder.expunge(); } - MimeMessageEx icopy = MessageHelper.from(message, attachments, isession); + MimeMessageEx icopy = MessageHelper.from(this, message, attachments, isession); Folder itarget = istore.getFolder(target.name); itarget.appendMessages(new Message[]{icopy}); } @@ -909,8 +908,6 @@ public class ServiceSynchronize extends LifecycleService { EntityIdentity ident = db.identity().getIdentity(message.identity); EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying)); List attachments = db.attachment().getAttachments(message.id); - for (EntityAttachment attachment : attachments) - attachment.content = db.attachment().getContent(attachment.id); if (!ident.synchronize) { // Message will remain in outbox @@ -924,9 +921,9 @@ public class ServiceSynchronize extends LifecycleService { // Create message MimeMessage imessage; if (reply == null) - imessage = MessageHelper.from(message, attachments, isession); + imessage = MessageHelper.from(this, message, attachments, isession); else - imessage = MessageHelper.from(message, reply, attachments, isession); + imessage = MessageHelper.from(this, message, reply, attachments, isession); if (ident.replyto != null) imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)}); @@ -1000,29 +997,51 @@ public class ServiceSynchronize extends LifecycleService { MessageHelper helper = new MessageHelper((MimeMessage) imessage); EntityAttachment a = helper.getAttachments().get(sequence - 1); + // Build filename + File dir = new File(getFilesDir(), "attachments"); + dir.mkdir(); + File file = new File(dir, Long.toString(attachment.id)); + // Download attachment - InputStream is = a.part.getInputStream(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; - for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { - os.write(buffer, 0, len); - - // Update progress - if (attachment.size != null) { - attachment.progress = os.size() * 100 / attachment.size; - db.attachment().updateAttachment(attachment); - Log.i(Helper.TAG, folder.name + " progress %=" + attachment.progress); + InputStream is = null; + OutputStream os = null; + try { + is = a.part.getInputStream(); + os = new BufferedOutputStream(new FileOutputStream(file)); + + int size = 0; + byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; + for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { + size += len; + os.write(buffer, 0, len); + + // Update progress + if (attachment.size != null) { + attachment.progress = size * 100 / attachment.size; + db.attachment().updateAttachment(attachment); + Log.i(Helper.TAG, folder.name + " progress %=" + attachment.progress); + } } - } - // Store attachment data - attachment.progress = null; - attachment.content = os.toByteArray(); + // Store attachment data + attachment.size = size; + attachment.progress = null; + attachment.filename = file.getName(); + } finally { + try { + if (is != null) + is.close(); + } finally { + if (os != null) + os.close(); + } + } db.attachment().updateAttachment(attachment); - Log.i(Helper.TAG, folder.name + " downloaded bytes=" + attachment.content.length); + Log.i(Helper.TAG, folder.name + " downloaded bytes=" + attachment.size); } catch (Throwable ex) { // Reset progress on failure attachment.progress = null; + attachment.filename = null; db.attachment().updateAttachment(attachment); throw ex; } diff --git a/app/src/main/java/eu/faircode/email/TupleAttachment.java b/app/src/main/java/eu/faircode/email/TupleAttachment.java deleted file mode 100644 index 61b2682d..00000000 --- a/app/src/main/java/eu/faircode/email/TupleAttachment.java +++ /dev/null @@ -1,19 +0,0 @@ -package eu.faircode.email; - -import androidx.annotation.NonNull; - -public class TupleAttachment { - @NonNull - public Long id; - @NonNull - public Long message; - @NonNull - public Integer sequence; - public String name; - @NonNull - public String type; - public Integer size; - public Integer progress; - @NonNull - public boolean content; -} diff --git a/app/src/main/res/layout/item_attachment.xml b/app/src/main/res/layout/item_attachment.xml index a7c822d6..0a4a2670 100644 --- a/app/src/main/res/layout/item_attachment.xml +++ b/app/src/main/res/layout/item_attachment.xml @@ -51,6 +51,26 @@ app:layout_constraintStart_toEndOf="@id/tvSize" app:layout_constraintTop_toTopOf="parent" /> + + + + + app:layout_constraintTop_toBottomOf="@id/tvType" /> \ No newline at end of file diff --git a/app/src/main/res/xml/fileprovider_paths.xml b/app/src/main/res/xml/fileprovider_paths.xml index 430a8850..e9dbf344 100644 --- a/app/src/main/res/xml/fileprovider_paths.xml +++ b/app/src/main/res/xml/fileprovider_paths.xml @@ -1,6 +1,6 @@ -