Fixes and improvements

- Last loaders/executors have been gone
- Improved debug info
- Fixed multiple draft saves
pull/50/head
M66B 6 years ago
parent 4a43ebaafc
commit d9875de010

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "7814b856d44afe54b8912106df1e673b",
"identityHash": "262ca4c3e0dbf6673b00b8b19fc219de",
"entities": [
{
"tableName": "identity",
@ -660,7 +660,7 @@
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `error` TEXT, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , 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, `folder` INTEGER NOT NULL, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
@ -693,10 +693,10 @@
"notNull": true
},
{
"fieldPath": "error",
"columnName": "error",
"affinity": "TEXT",
"notNull": false
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
@ -751,7 +751,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, \"7814b856d44afe54b8912106df1e673b\")"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"262ca4c3e0dbf6673b00b8b19fc219de\")"
]
}
}

@ -24,6 +24,7 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@ -40,18 +41,17 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.ViewHolder> {
private Context context;
private ExecutorService executor = Executors.newCachedThreadPool();
private LifecycleOwner owner;
private List<TupleAttachment> all = new ArrayList<>();
private List<TupleAttachment> filtered = new ArrayList<>();
@ -140,58 +140,84 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
return;
}
Bundle args = new Bundle();
args.putLong("id", attachment.id);
args.putSerializable("file", file);
args.putSerializable("dir", dir);
// View
executor.submit(new Runnable() {
new SimpleTask<Void>() {
@Override
public void run() {
try {
// Create file
if (!file.exists()) {
dir.mkdir();
file.createNewFile();
// Get attachment content
byte[] content = DB.getInstance(context).attachment().getContent(attachment.id);
// Write attachment content to file
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
fos.write(content);
} finally {
if (fos != null)
fos.close();
}
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();
}
// Start viewer
context.startActivity(intent);
} catch (Throwable ex) {
Log.i(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
}
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.progress == null)
// Download
executor.submit(new Runnable() {
if (attachment.progress == null) {
Bundle args = new Bundle();
args.putLong("id", attachment.id);
args.putLong("message", attachment.message);
args.putInt("sequence", attachment.sequence);
new SimpleTask<Void>() {
@Override
public void run() {
protected Void onLoad(Context context, Bundle args) {
long id = args.getLong("id");
long message = args.getLong("message");
long sequence = args.getInt("sequence");
// No need for a transaction
DB db = DB.getInstance(context);
db.attachment().setProgress(attachment.id, 0);
db.attachment().setProgress(id, 0);
EntityMessage message = db.message().getMessage(attachment.message);
EntityOperation.queue(db, message, EntityOperation.ATTACHMENT, attachment.sequence);
EntityMessage msg = db.message().getMessage(message);
EntityOperation.queue(db, msg, EntityOperation.ATTACHMENT, sequence);
EntityOperation.process(context);
return null;
}
});
}.load(context, owner, args);
}
}
}
}
AdapterAttachment(Context context) {
AdapterAttachment(Context context, LifecycleOwner owner) {
this.context = context;
this.owner = owner;
setHasStableIds(true);
}

@ -31,7 +31,14 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
@ -39,8 +46,11 @@ import androidx.recyclerview.widget.RecyclerView;
public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMessage.ViewHolder> {
private Context context;
private LifecycleOwner owner;
private ViewType viewType;
private boolean debug;
private DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.LONG);
enum ViewType {FOLDER, THREAD}
@ -87,7 +97,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
pbLoading.setVisibility(View.VISIBLE);
}
private void bindTo(TupleMessageEx message) {
private void bindTo(final TupleMessageEx message) {
pbLoading.setVisibility(View.GONE);
if (EntityFolder.DRAFTS.equals(message.folderType) ||
@ -111,8 +121,25 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
tvCount.setVisibility(View.VISIBLE);
}
if (debug)
message.error += (message.ui_hide ? " HIDDEN " : " ") + message.msgid + "/" + message.uid + "/" + message.id;
if (debug) {
DB db = DB.getInstance(context);
db.operation().getOperationsByMessage(message.id).removeObservers(owner);
db.operation().getOperationsByMessage(message.id).observe(owner, new Observer<List<EntityOperation>>() {
@Override
public void onChanged(List<EntityOperation> operations) {
String text = message.error +
"\n" + message.id + " " + df.format(new Date(message.received)) +
"\n" + (message.ui_hide ? "HIDDEN " : " ") + message.uid + "/" + message.id +
"\n" + message.msgid;
for (EntityOperation op : operations)
text += "\n" + op.name + " " + df.format(new Date(op.created));
tvError.setText(text);
tvError.setVisibility(View.VISIBLE);
}
});
}
tvError.setText(message.error);
tvError.setVisibility(message.error == null ? View.GONE : View.VISIBLE);
@ -165,9 +192,10 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
}
}
AdapterMessage(Context context, ViewType viewType) {
AdapterMessage(Context context, LifecycleOwner owner, ViewType viewType) {
super(DIFF_CALLBACK);
this.context = context;
this.owner = owner;
this.viewType = viewType;
this.debug = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("debug", false);
}

@ -98,6 +98,7 @@ public interface DaoMessage {
@Query("SELECT uid FROM message WHERE folder = :folder AND received >= :received AND NOT uid IS NULL")
List<Long> getUids(long folder, long received);
// in case of duplicate message IDs
@Insert(onConflict = OnConflictStrategy.REPLACE)
long insertMessage(EntityMessage message);

@ -21,15 +21,18 @@ package eu.faircode.email;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@Dao
public interface DaoOperation {
@Query("SELECT * FROM operation WHERE message = :message ORDER BY id")
LiveData<List<EntityOperation>> getOperationsByMessage(long message);
@Query("SELECT * FROM operation WHERE folder = :folder ORDER BY id")
List<EntityOperation> getOperations(long folder);
List<EntityOperation> getOperationsByFolder(long folder);
@Query("SELECT COUNT(id) FROM operation WHERE folder = :folder")
int getOperationCount(long folder);
@ -37,9 +40,6 @@ public interface DaoOperation {
@Insert
long insertOperation(EntityOperation operation);
@Update
void updateOperation(EntityOperation operation);
@Query("DELETE FROM operation WHERE id = :id")
void deleteOperation(long id);
}

@ -26,6 +26,7 @@ import android.util.Log;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import androidx.annotation.NonNull;
@ -61,7 +62,8 @@ public class EntityOperation {
public String name;
@NonNull
public String args;
public String error;
@NonNull
public Long created;
public static final String SEEN = "seen";
public static final String ADD = "add";
@ -96,6 +98,7 @@ public class EntityOperation {
operation.message = message.id;
operation.name = name;
operation.args = jsonArray.toString();
operation.created = new Date().getTime();
operation.id = db.operation().insertOperation(operation);
Intent intent = new Intent();
@ -129,8 +132,7 @@ public class EntityOperation {
return (this.folder.equals(other.folder) &&
this.message.equals(other.message) &&
this.name.equals(other.name) &&
this.args.equals(other.args) &&
(this.error == null ? other.error == null : this.error.equals(other.error)));
this.args.equals(other.args));
} else
return false;
}

@ -239,7 +239,7 @@ public class FragmentCompose extends FragmentEx {
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAttachment.setLayoutManager(llm);
adapter = new AdapterAttachment(getContext());
adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner());
rvAttachment.setAdapter(adapter);
return view;
@ -757,36 +757,40 @@ public class FragmentCompose extends FragmentEx {
EntityOperation.queue(db, draft, EntityOperation.MOVE, trash.id);
} else if (action == R.id.action_save) {
// Save message ID
String msgid = draft.msgid;
// Save attachments
List<EntityAttachment> attachments = db.attachment().getAttachments(draft.id);
for (EntityAttachment attachment : attachments)
attachment.content = db.attachment().getContent(attachment.id);
// Delete previous draft
draft.msgid = null;
draft.ui_hide = true;
db.message().updateMessage(draft);
EntityOperation.queue(db, draft, EntityOperation.DELETE);
// Create new draft
draft.id = null;
draft.uid = null;
draft.msgid = msgid;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
if (draft.uid == null)
db.message().updateMessage(draft);
else {
// Save message ID
String msgid = draft.msgid;
// Save attachments
List<EntityAttachment> attachments = db.attachment().getAttachments(draft.id);
for (EntityAttachment attachment : attachments)
attachment.content = db.attachment().getContent(attachment.id);
// Delete previous draft
draft.msgid = null;
draft.ui_hide = true;
db.message().updateMessage(draft);
EntityOperation.queue(db, draft, EntityOperation.DELETE);
// Create new draft
draft.id = null;
draft.uid = null;
draft.msgid = msgid;
draft.ui_hide = false;
draft.id = db.message().insertMessage(draft);
// Restore attachments
for (EntityAttachment attachment : attachments) {
attachment.message = draft.id;
db.attachment().insertAttachment(attachment);
}
// Restore attachments
for (EntityAttachment attachment : attachments) {
attachment.message = draft.id;
db.attachment().insertAttachment(attachment);
EntityOperation.queue(db, draft, EntityOperation.ADD);
}
EntityOperation.queue(db, draft, EntityOperation.ADD);
} else if (action == R.id.action_send) {
// Check data
if (draft.identity == null)

@ -66,7 +66,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class FragmentMessage extends FragmentEx {
private boolean debug;
private TextView tvFrom;
private TextView tvTime;
private TextView tvSubject;
@ -86,6 +85,7 @@ public class FragmentMessage extends FragmentEx {
private AdapterAttachment adapter;
private boolean debug;
private DateFormat df = SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
@Override
@ -93,11 +93,10 @@ public class FragmentMessage extends FragmentEx {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_message, container, false);
this.debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
// Get arguments
Bundle args = getArguments();
final long id = (args == null ? -1 : args.getLong("id"));
debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
// Get controls
tvFrom = view.findViewById(R.id.tvFrom);
@ -226,7 +225,7 @@ public class FragmentMessage extends FragmentEx {
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvAttachment.setLayoutManager(llm);
adapter = new AdapterAttachment(getContext());
adapter = new AdapterAttachment(getContext(), getViewLifecycleOwner());
rvAttachment.setAdapter(adapter);
return view;
@ -246,21 +245,19 @@ public class FragmentMessage extends FragmentEx {
db.message().liveMessage(id).observe(getViewLifecycleOwner(), new Observer<TupleMessageEx>() {
@Override
public void onChanged(@Nullable final TupleMessageEx message) {
if (message == null || message.ui_hide) {
if (message == null || (!(debug && BuildConfig.DEBUG) && message.ui_hide)) {
// Message gone (moved, deleted)
if (FragmentMessage.this.isVisible())
getFragmentManager().popBackStack();
} else {
setSubtitle(Helper.localizeFolderName(getContext(), message.folderName));
String extra = (debug ? (message.ui_hide ? "HIDDEN " : "") + message.uid + "/" + message.id + " " : "");
tvFrom.setText(message.from == null ? null : MessageHelper.getFormattedAddresses(message.from, true));
tvTime.setText(message.sent == null ? null : df.format(new Date(message.sent)));
tvSubject.setText(message.subject);
tvCount.setText(extra + Integer.toString(message.count));
tvCount.setVisibility(debug || message.count > 1 ? View.VISIBLE : View.GONE);
tvCount.setText(Integer.toString(message.count));
tvCount.setVisibility(message.count > 1 ? View.VISIBLE : View.GONE);
tvTo.setText(message.to == null ? null : MessageHelper.getFormattedAddresses(message.to, true));
tvCc.setText(message.cc == null ? null : MessageHelper.getFormattedAddresses(message.cc, true));
@ -287,9 +284,6 @@ public class FragmentMessage extends FragmentEx {
}
});
if (debug)
message.error += (message.ui_hide ? " HIDDEN " : " ") + message.msgid + "/" + message.uid + "/" + message.id;
tvError.setText(message.error);
tvError.setVisibility(message.error == null ? View.GONE : View.VISIBLE);
@ -629,12 +623,16 @@ public class FragmentMessage extends FragmentEx {
try {
db.beginTransaction();
EntityMessage message = db.message().getMessage(id);
message.ui_hide = true;
db.message().updateMessage(message);
if (debug && BuildConfig.DEBUG)
db.message().deleteMessage(id);
else {
EntityMessage message = db.message().getMessage(id);
message.ui_hide = true;
db.message().updateMessage(message);
EntityFolder trash = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
EntityOperation.queue(db, message, EntityOperation.MOVE, trash.id);
EntityFolder trash = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
EntityOperation.queue(db, message, EntityOperation.MOVE, trash.id);
}
db.setTransactionSuccessful();
} finally {

@ -76,7 +76,9 @@ public class FragmentMessages extends FragmentEx {
LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvMessage.setLayoutManager(llm);
adapter = new AdapterMessage(getContext(),
adapter = new AdapterMessage(
getContext(),
getViewLifecycleOwner(),
thread < 0
? AdapterMessage.ViewType.FOLDER
: AdapterMessage.ViewType.THREAD);

@ -710,7 +710,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(Helper.TAG, folder.name + " start process");
DB db = DB.getInstance(this);
List<EntityOperation> ops = db.operation().getOperations(folder.id);
List<EntityOperation> ops = db.operation().getOperationsByFolder(folder.id);
Log.i(Helper.TAG, folder.name + " pending operations=" + ops.size());
for (EntityOperation op : ops)
try {
@ -719,11 +719,12 @@ public class ServiceSynchronize extends LifecycleService {
" msg=" + op.message +
" args=" + op.args);
EntityMessage message = db.message().getMessage(op.message);
if (message == null)
throw new MessageRemovedException();
try {
JSONArray jargs = new JSONArray(op.args);
EntityMessage message = db.message().getMessage(op.message);
if (message == null)
throw new MessageRemovedException();
if (EntityOperation.SEEN.equals(op.name))
doSeen(folder, ifolder, message, jargs);
@ -735,7 +736,7 @@ public class ServiceSynchronize extends LifecycleService {
doMove(folder, isession, istore, ifolder, message, jargs, db);
else if (EntityOperation.DELETE.equals(op.name))
doDelete(folder, ifolder, message, db);
doDelete(folder, ifolder, message, jargs, db);
else if (EntityOperation.SEND.equals(op.name))
doSend(db, message);
@ -749,8 +750,8 @@ public class ServiceSynchronize extends LifecycleService {
// Operation succeeded
db.operation().deleteOperation(op.id);
} catch (Throwable ex) {
op.error = Helper.formatThrowable(ex);
db.operation().updateOperation(op);
message.error = Helper.formatThrowable(ex);
db.message().updateMessage(message);
if (BuildConfig.DEBUG && ex instanceof NullPointerException) {
db.operation().deleteOperation(op.id);
@ -786,9 +787,8 @@ public class ServiceSynchronize extends LifecycleService {
private void doSeen(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs) throws MessagingException, JSONException {
// Mark message (un)seen
if (message.uid == null) {
Log.w(Helper.TAG, folder.name + " local op seen id=" + message.id + " uid=" + message.uid);
Log.w(Helper.TAG, folder.name + " local op seen id=" + message.id);
return;
}
@ -814,12 +814,8 @@ public class ServiceSynchronize extends LifecycleService {
private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException {
// Move message
if (BuildConfig.DEBUG && message.uid == null) {
Log.w(Helper.TAG, "Move local message id=" + message.id);
db.message().deleteMessage(message.id);
return;
}
if (message.uid == null)
throw new IllegalArgumentException("MOVE local id=" + message.id);
long id = jargs.getLong(0);
EntityFolder target = db.folder().getFolder(id);
@ -850,18 +846,17 @@ public class ServiceSynchronize extends LifecycleService {
}
}
private void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
private void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
// Delete message
if (message.uid == null)
Log.w(Helper.TAG, folder.name + " Delete local message id=" + message.id);
else {
Message imessage = ifolder.getMessageByUID(message.uid);
if (imessage == null)
throw new MessageRemovedException();
throw new IllegalArgumentException("DELETE local id=" + message.id);
imessage.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
}
Message imessage = ifolder.getMessageByUID(message.uid);
if (imessage == null)
throw new MessageRemovedException();
imessage.setFlag(Flags.Flag.DELETED, true);
ifolder.expunge();
db.message().deleteMessage(message.id);
}

@ -39,6 +39,10 @@ import androidx.lifecycle.OnLifecycleEvent;
public abstract class SimpleTask<T> implements LifecycleObserver {
private boolean alive = true;
public void load(Context context, LifecycleOwner owner, Bundle args) {
run(context, owner, args);
}
public void load(AppCompatActivity activity, Bundle args) {
run(activity, activity, args);
}

@ -71,7 +71,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginStart="6dp"
android:text="error"
android:text="error\ndebug info\noperation 1\n operation 2"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"

Loading…
Cancel
Save