Store messages in files

This commit is contained in:
M66B
2018-08-19 06:53:56 +00:00
parent ee7b41f7b4
commit d884c9c7ec
12 changed files with 185 additions and 107 deletions

View File

@@ -225,6 +225,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
file.delete();
String body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
EntityMessage draft = null;
DB db = DB.getInstance(context);
@@ -239,12 +241,12 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack
draft.msgid = EntityMessage.generateMessageId();
draft.to = new Address[]{Helper.myAddress()};
draft.subject = context.getString(R.string.app_name) + " crash log";
draft.body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body);
}
EntityOperation.queue(db, draft, EntityOperation.ADD);

View File

@@ -64,7 +64,6 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
TextView tvSize;
ImageView ivStatus;
TextView tvType;
TextView tvFile;
ProgressBar progressbar;
ViewHolder(View itemView) {
@@ -76,7 +75,6 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
tvSize = itemView.findViewById(R.id.tvSize);
ivStatus = itemView.findViewById(R.id.ivStatus);
tvType = itemView.findViewById(R.id.tvType);
tvFile = itemView.findViewById(R.id.tvFile);
progressbar = itemView.findViewById(R.id.progressbar);
}
@@ -97,15 +95,15 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
tvSize.setText(Helper.humanReadableByteCount(attachment.size, false));
tvSize.setVisibility(attachment.size == null ? View.GONE : View.VISIBLE);
if (attachment.filename == null) {
if (attachment.available) {
ivStatus.setImageResource(R.drawable.baseline_visibility_24);
ivStatus.setVisibility(View.VISIBLE);
} else {
if (attachment.progress == null) {
ivStatus.setImageResource(R.drawable.baseline_get_app_24);
ivStatus.setVisibility(View.VISIBLE);
} else
ivStatus.setVisibility(View.GONE);
} else {
ivStatus.setImageResource(R.drawable.baseline_visibility_24);
ivStatus.setVisibility(View.VISIBLE);
}
ivDelete.setVisibility(readonly ? View.GONE : View.VISIBLE);
@@ -113,12 +111,10 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
if (attachment.progress != null)
progressbar.setProgress(attachment.progress);
progressbar.setVisibility(
attachment.progress == null || attachment.filename != null ? View.GONE : View.VISIBLE);
attachment.progress == null || attachment.available ? View.GONE : View.VISIBLE);
tvType.setText(attachment.type);
tvFile.setText(attachment.filename);
tvType.setVisibility(debug ? View.VISIBLE : View.GONE);
tvFile.setVisibility(debug ? View.VISIBLE : View.GONE);
}
@Override
@@ -137,7 +133,7 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
protected Void onLoad(Context context, Bundle args) {
DB.getInstance(context).attachment().deleteAttachment(attachment.id);
File dir = new File(context.getFilesDir(), "attachments");
File file = new File(dir, attachment.filename);
File file = new File(dir, attachment.id.toString());
file.delete();
return null;
@@ -145,7 +141,39 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
}.load(context, owner, args);
} else {
if (attachment.filename == null) {
if (attachment.available) {
// Build file name
File dir = new File(context.getFilesDir(), "attachments");
File file = new File(dir, attachment.id.toString());
// https://developer.android.com/reference/android/support/v4/content/FileProvider
Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file);
Log.i(Helper.TAG, "uri=" + uri);
// Build intent
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, attachment.type);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.i(Helper.TAG, "Sharing " + file + " type=" + attachment.type);
Log.i(Helper.TAG, "Intent=" + intent);
//context.startActivity(Intent.createChooser(intent, attachment.name));
// Set permissions
List<ResolveInfo> targets = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : targets) {
Log.i(Helper.TAG, "Target=" + resolveInfo);
context.grantUriPermission(resolveInfo.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
// Check if viewer available
if (targets.size() == 0) {
Toast.makeText(context, R.string.title_no_viewer, Toast.LENGTH_LONG).show();
return;
}
context.startActivity(intent);
} else {
if (attachment.progress == null) {
Bundle args = new Bundle();
args.putLong("id", attachment.id);
@@ -179,38 +207,6 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
}
}.load(context, owner, args);
}
} else {
// Build file name
File dir = new File(context.getFilesDir(), "attachments");
File file = new File(dir, attachment.filename);
// https://developer.android.com/reference/android/support/v4/content/FileProvider
Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file);
Log.i(Helper.TAG, "uri=" + uri);
// Build intent
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, attachment.type);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Log.i(Helper.TAG, "Sharing " + file + " type=" + attachment.type);
Log.i(Helper.TAG, "Intent=" + intent);
//context.startActivity(Intent.createChooser(intent, attachment.name));
// Set permissions
List<ResolveInfo> targets = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : targets) {
Log.i(Helper.TAG, "Target=" + resolveInfo);
context.grantUriPermission(resolveInfo.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
// Check if viewer available
if (targets.size() == 0) {
Toast.makeText(context, R.string.title_no_viewer, Toast.LENGTH_LONG).show();
return;
}
context.startActivity(intent);
}
}
}

View File

@@ -71,7 +71,7 @@ public abstract class DB extends RoomDatabase {
private static DB sInstance;
private static final String DB_NAME = "fairemail";
private static final String DB_NAME = "email";
public static synchronized DB getInstance(Context context) {
if (sInstance == null)

View File

@@ -54,7 +54,8 @@ public class EntityAttachment {
public String type;
public Integer size;
public Integer progress;
public String filename;
@NonNull
public Boolean available = false;
@Ignore
BodyPart part;
@@ -69,7 +70,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.filename == null ? other.filename == null : this.filename.equals(other.filename)));
this.available.equals(other.available));
} else
return false;
}

View File

@@ -19,6 +19,15 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import java.util.Random;
@@ -76,7 +85,6 @@ public class EntityMessage {
public Address[] bcc;
public Address[] reply;
public String subject;
public String body;
public Long sent; // compose = null
@NonNull
public Long received; // compose = stored
@@ -100,6 +108,52 @@ public class EntityMessage {
return sb.toString();
}
void write(Context context, String body) throws IOException {
File dir = new File(context.getFilesDir(), "messages");
dir.mkdir();
File file = new File(dir, id.toString());
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter(file));
out.write(body);
} finally {
if (out != null)
try {
out.close();
} catch (IOException e) {
Log.e(Helper.TAG, e + "\n" + Log.getStackTraceString(e));
}
}
}
String read(Context context) throws IOException {
return read(context, this.id);
}
static String read(Context context, Long id) throws IOException {
File dir = new File(context.getFilesDir(), "messages");
File file = new File(dir, id.toString());
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader(file));
StringBuilder body = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
body.append(line);
body.append('\n');
}
return body.toString();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
}
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof EntityMessage) {
@@ -119,7 +173,6 @@ public class EntityMessage {
equal(this.bcc, other.bcc) &&
equal(this.reply, other.reply) &&
(this.subject == null ? other.subject == null : this.subject.equals(other.subject)) &&
(this.body == null ? other.body == null : this.body.equals(other.body)) &&
(this.sent == null ? other.sent == null : this.sent.equals(other.sent)) &&
this.received.equals(other.received) &&
this.seen.equals(other.seen) &&

View File

@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,6 +31,7 @@ import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
@@ -80,6 +82,8 @@ public class FragmentAbout extends FragmentEx {
sb.append(Helper.getLogcat());
String body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
EntityMessage draft;
DB db = DB.getInstance(context);
@@ -96,16 +100,19 @@ public class FragmentAbout extends FragmentEx {
draft.msgid = EntityMessage.generateMessageId();
draft.to = new Address[]{Helper.myAddress()};
draft.subject = context.getString(R.string.app_name) + " debug info";
draft.body = "<pre>" + sb.toString().replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
draft.seen = false;
draft.ui_seen = false;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body);
EntityOperation.queue(db, draft, EntityOperation.ADD);
db.setTransactionSuccessful();
} catch (IOException ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
return null;
} finally {
db.endTransaction();
}
@@ -118,9 +125,10 @@ public class FragmentAbout extends FragmentEx {
@Override
protected void onLoaded(Bundle args, Long id) {
btnDebugInfo.setEnabled(true);
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("action", "edit")
.putExtra("id", id));
if (id != null)
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("action", "edit")
.putExtra("id", id));
}
@Override

View File

@@ -30,6 +30,7 @@ import android.os.Handler;
import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
@@ -516,7 +517,7 @@ public class FragmentCompose extends FragmentEx {
attachment.size = size;
attachment.progress = null;
attachment.filename = file.getName();
attachment.available = true;
db.attachment().updateAttachment(attachment);
} finally {
try {
@@ -530,7 +531,6 @@ public class FragmentCompose extends FragmentEx {
} catch (Throwable ex) {
// Reset progress on failure
attachment.progress = null;
attachment.filename = null;
db.attachment().updateAttachment(attachment);
throw ex;
}
@@ -571,7 +571,7 @@ public class FragmentCompose extends FragmentEx {
private SimpleTask<EntityMessage> draftLoader = new SimpleTask<EntityMessage>() {
@Override
protected EntityMessage onLoad(Context context, Bundle args) {
protected EntityMessage onLoad(Context context, Bundle args) throws IOException {
String action = args.getString("action");
long id = args.getLong("id", -1);
long account = args.getLong("account", -1);
@@ -618,6 +618,8 @@ public class FragmentCompose extends FragmentEx {
if (drafts == null)
throw new IllegalArgumentException("no drafts folder");
String body = "";
draft = new EntityMessage();
draft.account = account;
draft.folder = drafts.id;
@@ -640,21 +642,21 @@ public class FragmentCompose extends FragmentEx {
if ("reply".equals(action) || "reply_all".equals(action)) {
draft.subject = context.getString(R.string.title_subject_reply, ref.subject);
draft.body = String.format("<br><br>%s %s:<br><br>%s",
body = String.format("<br><br>%s %s:<br><br>%s",
Html.escapeHtml(new Date().toString()),
Html.escapeHtml(TextUtils.join(", ", draft.to)),
HtmlHelper.sanitize(context, ref.body, true));
HtmlHelper.sanitize(context, ref.read(context), true));
} else if ("forward".equals(action)) {
draft.subject = context.getString(R.string.title_subject_forward, ref.subject);
draft.body = String.format("<br><br>%s %s:<br><br>%s",
body = String.format("<br><br>%s %s:<br><br>%s",
Html.escapeHtml(new Date().toString()),
Html.escapeHtml(TextUtils.join(", ", ref.from)),
HtmlHelper.sanitize(context, ref.body, true));
HtmlHelper.sanitize(context, ref.read(context), true));
}
}
if ("new".equals(action))
draft.body = "";
body = "";
draft.received = new Date().getTime();
draft.seen = false;
@@ -662,6 +664,7 @@ public class FragmentCompose extends FragmentEx {
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
draft.write(context, body);
EntityOperation.queue(db, draft, EntityOperation.ADD);
@@ -687,7 +690,22 @@ public class FragmentCompose extends FragmentEx {
etBcc.setText(draft.bcc == null ? null : TextUtils.join(", ", draft.bcc));
etSubject.setText(draft.subject);
etBody.setText(TextUtils.isEmpty(draft.body) ? null : Html.fromHtml(draft.body));
etBody.setText(null);
Bundle a = new Bundle();
a.putLong("id", draft.id);
new SimpleTask<Spanned>() {
@Override
protected Spanned onLoad(Context context, Bundle args) throws Throwable {
return Html.fromHtml(EntityMessage.read(context, args.getLong("id")));
}
@Override
protected void onLoaded(Bundle args, Spanned body) {
etBody.setText(body);
}
}.load(FragmentCompose.this, a);
getActivity().invalidateOptionsMenu();
Helper.setViewsEnabled(view, true);
@@ -836,14 +854,16 @@ public class FragmentCompose extends FragmentEx {
draft.cc = acc;
draft.bcc = abcc;
draft.subject = subject;
draft.body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
draft.received = new Date().getTime();
body = "<pre>" + body.replaceAll("\\r?\\n", "<br />") + "</pre>";
// Execute action
if (action == R.id.action_trash) {
draft.ui_seen = true;
draft.ui_hide = true;
db.message().updateMessage(draft);
draft.write(context, body);
EntityOperation.queue(db, draft, EntityOperation.SEEN, true);
@@ -857,11 +877,13 @@ public class FragmentCompose extends FragmentEx {
return null;
db.message().updateMessage(draft);
draft.write(context, body);
EntityOperation.queue(db, draft, EntityOperation.ADD);
} else if (action == R.id.action_send) {
db.message().updateMessage(draft);
draft.write(context, body);
// Check data
if (draft.identity == null)
@@ -876,7 +898,7 @@ public class FragmentCompose extends FragmentEx {
// Save attachments
List<EntityAttachment> attachments = db.attachment().getAttachments(draft.id);
for (EntityAttachment attachment : attachments)
if (attachment.filename == null)
if (!attachment.available)
throw new IllegalArgumentException(context.getString(R.string.title_attachments_missing));
// Delete draft (cannot move to outbox)

View File

@@ -29,6 +29,7 @@ import android.preference.PreferenceManager;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
@@ -339,9 +340,23 @@ public class FragmentMessage extends FragmentEx {
: R.drawable.baseline_visibility_24);
actionSeen.setTitle(message.ui_seen ? R.string.title_unseen : R.string.title_seen);
tvBody.setText(message.body == null
? null
: Html.fromHtml(HtmlHelper.sanitize(getContext(), message.body, false)));
tvBody.setText(null);
Bundle args = new Bundle();
args.putLong("id", message.id);
new SimpleTask<Spanned>() {
@Override
protected Spanned onLoad(Context context, Bundle args) throws Throwable {
String body = EntityMessage.read(context, args.getLong("id"));
return Html.fromHtml(HtmlHelper.sanitize(getContext(), body, false));
}
@Override
protected void onLoaded(Bundle args, Spanned body) {
tvBody.setText(body);
}
}.load(FragmentMessage.this, args);
db.folder().liveFolders(message.account).removeObservers(getViewLifecycleOwner());
db.folder().liveFolders(message.account).observe(getViewLifecycleOwner(), new Observer<List<TupleFolderEx>>() {

View File

@@ -93,7 +93,7 @@ public class MessageHelper {
return props;
}
static MimeMessageEx from(Context context, EntityMessage message, List<EntityAttachment> attachments, Session isession) throws MessagingException {
static MimeMessageEx from(Context context, EntityMessage message, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
MimeMessageEx imessage = new MimeMessageEx(isession, message.msgid);
imessage.setFlag(Flags.Flag.SEEN, message.seen);
@@ -115,25 +115,22 @@ public class MessageHelper {
// TODO: plain message?
if (message.body == null)
throw new IllegalArgumentException("null message");
if (attachments.size() == 0)
imessage.setText(message.body, Charset.defaultCharset().name(), "html");
imessage.setText(message.read(context), Charset.defaultCharset().name(), "html");
else {
Multipart multipart = new MimeMultipart();
BodyPart bpMessage = new MimeBodyPart();
bpMessage.setContent(message.body, "text/html; charset=" + Charset.defaultCharset().name());
bpMessage.setContent(message.read(context), "text/html; charset=" + Charset.defaultCharset().name());
multipart.addBodyPart(bpMessage);
for (final EntityAttachment attachment : attachments)
if (attachment.filename != null) {
if (attachment.available) {
BodyPart bpAttachment = new MimeBodyPart();
bpAttachment.setFileName(attachment.name);
File dir = new File(context.getFilesDir(), "attachments");
File file = new File(dir, attachment.filename);
File file = new File(dir, attachment.id.toString());
FileDataSource dataSource = new FileDataSource(file);
dataSource.setFileTypeMap(new FileTypeMap() {
@Override
@@ -159,7 +156,7 @@ public class MessageHelper {
return imessage;
}
static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List<EntityAttachment> attachments, Session isession) throws MessagingException {
static MimeMessageEx from(Context context, EntityMessage message, EntityMessage reply, List<EntityAttachment> attachments, Session isession) throws MessagingException, IOException {
MimeMessageEx imessage = from(context, message, attachments, isession);
imessage.addHeader("In-Reply-To", reply.msgid);
imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid);

View File

@@ -851,7 +851,7 @@ public class ServiceSynchronize extends LifecycleService {
db.message().setMessageSeen(message.id, seen);
}
private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException {
// Append message
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
@@ -871,7 +871,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, "Appended uid=" + message.uid);
}
private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException {
private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
// Move message
long id = jargs.getLong(0);
EntityFolder target = db.folder().getFolder(id);
@@ -914,7 +914,7 @@ public class ServiceSynchronize extends LifecycleService {
db.message().deleteMessage(message.id);
}
private void doSend(Session isession, EntityMessage message, DB db) throws MessagingException {
private void doSend(Session isession, EntityMessage message, DB db) throws MessagingException, IOException {
// Send message
EntityIdentity ident = db.identity().getIdentity(message.identity);
EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying));
@@ -1033,7 +1033,7 @@ public class ServiceSynchronize extends LifecycleService {
// Store attachment data
attachment.size = size;
attachment.progress = null;
attachment.filename = file.getName();
attachment.available = true;
db.attachment().updateAttachment(attachment);
} finally {
try {
@@ -1048,7 +1048,6 @@ public class ServiceSynchronize extends LifecycleService {
} catch (Throwable ex) {
// Reset progress on failure
attachment.progress = null;
attachment.filename = null;
db.attachment().updateAttachment(attachment);
throw ex;
}
@@ -1296,7 +1295,6 @@ public class ServiceSynchronize extends LifecycleService {
message.bcc = helper.getBcc();
message.reply = helper.getReply();
message.subject = imessage.getSubject();
message.body = helper.getHtml();
message.received = imessage.getReceivedDate().getTime();
message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
message.seen = seen;
@@ -1304,6 +1302,7 @@ public class ServiceSynchronize extends LifecycleService {
message.ui_hide = false;
message.id = db.message().insertMessage(message);
message.write(this, helper.getHtml());
Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid);
int sequence = 0;