Browse messages on server, improvements

Fixes #115
pull/145/head
M66B 6 years ago
parent 0896a3589e
commit 4289576da0

@ -26,23 +26,23 @@ import android.util.Log;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.util.FolderClosedIOException;
import java.util.Date;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.mail.FetchProfile;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.Session;
import javax.mail.UIDFolder;
import javax.mail.search.AndTerm;
import javax.mail.search.BodyTerm;
import javax.mail.search.ComparisonTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.ReceivedDateTerm;
import javax.mail.search.SubjectTerm;
import androidx.lifecycle.GenericLifecycleObserver;
@ -54,16 +54,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private Context context;
private long fid;
private String search;
private int pageSize;
private Handler mainHandler;
private IBoundaryCallbackMessages intf;
private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
private boolean enabled = false;
private IMAPStore istore = null;
private IMAPFolder ifolder = null;
private Message[] imessages = null;
private static final int SEARCH_PAGE_SIZE = 5;
private int index;
private boolean searching = false;
interface IBoundaryCallbackMessages {
void onLoading();
@ -73,11 +73,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
void onError(Context context, Throwable ex);
}
BoundaryCallbackMessages(Context context, LifecycleOwner owner, long folder, String search, IBoundaryCallbackMessages intf) {
this.context = context;
BoundaryCallbackMessages(Context _context, LifecycleOwner owner, long folder, String search, int pageSize, IBoundaryCallbackMessages intf) {
this.context = _context;
this.fid = folder;
this.search = search;
this.mainHandler = new Handler(context.getMainLooper());
this.pageSize = pageSize;
this.mainHandler = new Handler(_context.getMainLooper());
this.intf = intf;
owner.getLifecycle().addObserver(new GenericLifecycleObserver() {
@ -88,12 +89,14 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
@Override
public void run() {
Log.i(Helper.TAG, "Boundary close");
DB.getInstance(context).message().deleteFoundMessages();
try {
if (istore != null)
istore.close();
} catch (Throwable ex) {
Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
context = null;
istore = null;
ifolder = null;
imessages = null;
@ -104,23 +107,29 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
});
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
boolean isSearching() {
return searching;
}
@Override
public void onZeroItemsLoaded() {
Log.i(Helper.TAG, "onZeroItemsLoaded");
load();
}
@Override
public void onItemAtEndLoaded(final TupleMessageEx itemAtEnd) {
Log.i(Helper.TAG, "onItemAtEndLoaded enabled=" + enabled);
if (!enabled)
return;
load(itemAtEnd.received);
Log.i(Helper.TAG, "onItemAtEndLoaded");
load();
}
void load(final long before) {
private void load() {
executor.submit(new Runnable() {
@Override
public void run() {
try {
searching = true;
mainHandler.post(new Runnable() {
@Override
public void run() {
@ -139,23 +148,34 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
Log.i(Helper.TAG, "Boundary connecting account=" + account.name);
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
Helper.connect(context, istore, account);
Log.i(Helper.TAG, "Boundary opening folder=" + folder.name);
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
Log.i(Helper.TAG, "Boundary searching=" + search + " before=" + new Date(before));
Log.i(Helper.TAG, "Boundary searching=" + search);
if (search == null)
imessages = ifolder.getMessages();
else
imessages = ifolder.search(
new AndTerm(
new ReceivedDateTerm(ComparisonTerm.LT, new Date(before)),
new OrTerm(
new FromStringTerm(search),
new OrTerm(
new SubjectTerm(search),
new BodyTerm(search)))));
new BodyTerm(search))));
Log.i(Helper.TAG, "Boundary found messages=" + imessages.length);
index = imessages.length - 1;
}
int count = 0;
while (index >= 0 && count < pageSize) {
Log.i(Helper.TAG, "Boundary index=" + index);
int from = Math.max(0, index - (pageSize - count) + 1);
Message[] isub = Arrays.copyOfRange(imessages, from, index + 1);
index -= (pageSize - count);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.FLAGS);
@ -164,25 +184,43 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
fp.add(IMAPFolder.FetchProfileItem.HEADERS);
fp.add(FetchProfile.Item.SIZE);
fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
ifolder.fetch(imessages, fp);
}
ifolder.fetch(isub, fp);
int count = 0;
int index = imessages.length - 1;
while (index >= 0) {
if (imessages[index].getReceivedDate().getTime() < before)
try {
Log.i(Helper.TAG, "Boundary sync uid=" + ifolder.getUID(imessages[index]));
ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) imessages[index], true);
if (++count >= SEARCH_PAGE_SIZE)
break;
db.beginTransaction();
for (int j = isub.length - 1; j >= 0; j--)
try {
long uid = ifolder.getUID(isub[j]);
Log.i(Helper.TAG, "Boundary sync uid=" + uid);
if (db.message().getMessageByUid(fid, uid) == null) {
ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) isub[j], true);
count++;
}
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
} catch (FolderClosedException ex) {
throw ex;
} catch (FolderClosedIOException ex) {
throw ex;
} catch (Throwable ex) {
Log.e(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
((IMAPMessage) isub[j]).invalidateHeaders();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
index--;
}
EntityOperation.process(context); // download small attachments
mainHandler.post(new Runnable() {
@Override
public void run() {
intf.onLoaded();
}
});
Log.i(Helper.TAG, "Boundary done");
} catch (final Throwable ex) {
@ -194,12 +232,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
}
});
} finally {
mainHandler.post(new Runnable() {
@Override
public void run() {
intf.onLoaded();
}
});
searching = false;
}
}
});

@ -218,9 +218,6 @@ public interface DaoMessage {
@Query("UPDATE message SET ui_found = :found WHERE id = :id")
int setMessageFound(long id, boolean found);
@Query("UPDATE message SET ui_found = 0 WHERE folder = :folder")
int resetFound(long folder);
@Query("UPDATE message SET content = :content WHERE id = :id")
int setMessageContent(long id, boolean content);

@ -136,7 +136,7 @@ public class FragmentFolder extends FragmentEx {
Properties props = MessageHelper.getSessionProperties(context, account.auth_type);
Session isession = Session.getInstance(props, null);
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
Helper.connect(context, istore, account);
if (folder == null) {
Log.i(Helper.TAG, "Creating folder=" + name);
@ -239,7 +239,7 @@ public class FragmentFolder extends FragmentEx {
Properties props = MessageHelper.getSessionProperties(context, account.auth_type);
Session isession = Session.getInstance(props, null);
istore = (IMAPStore) isession.getStore("imaps");
istore.connect(account.host, account.port, account.user, account.password);
Helper.connect(context, istore, account);
IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder.delete(false);

@ -84,7 +84,6 @@ public class FragmentMessages extends FragmentEx {
private AdapterMessage.ViewType viewType;
private LiveData<PagedList<TupleMessageEx>> messages = null;
private SearchState searchState = SearchState.Reset;
private BoundaryCallbackMessages searchCallback = null;
private ExecutorService executor = Executors.newCachedThreadPool(Helper.backgroundThreadFactory);
@ -93,8 +92,6 @@ public class FragmentMessages extends FragmentEx {
private static final int SEARCH_PAGE_SIZE = 10;
private static final int UNDO_TIMEOUT = 5000; // milliseconds
private enum SearchState {Reset, Database, Boundary}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -667,44 +664,47 @@ public class FragmentMessages extends FragmentEx {
messages = new LivePagedListBuilder<>(db.message().pagedUnifiedInbox(sort, debug), MESSAGES_PAGE_SIZE).build();
break;
case FOLDER:
messages = new LivePagedListBuilder<>(db.message().pagedFolder(folder, sort, false, debug), MESSAGES_PAGE_SIZE).build();
break;
case THREAD:
messages = new LivePagedListBuilder<>(db.message().pagedThread(thread, sort, debug), MESSAGES_PAGE_SIZE).build();
break;
if (searchCallback == null)
searchCallback = new BoundaryCallbackMessages(
getContext(), FragmentMessages.this,
folder, null, MESSAGES_PAGE_SIZE,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override
public void onLoading() {
pbWait.setVisibility(View.VISIBLE);
}
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
@Override
public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null ||
(viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0)) {
finish();
return;
public void onLoaded() {
pbWait.setVisibility(View.GONE);
}
Log.i(Helper.TAG, "Submit messages=" + messages.size());
adapter.submitList(messages);
@Override
public void onError(Context context, Throwable ex) {
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0) {
tvNoEmail.setVisibility(View.VISIBLE);
rvMessage.setVisibility(View.GONE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
Helper.unexpectedError(context, ex);
}
});
} else {
Log.i(Helper.TAG, "Search state=" + searchState);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(MESSAGES_PAGE_SIZE)
.setPrefetchDistance(MESSAGES_PAGE_SIZE)
.build();
LivePagedListBuilder<Integer, TupleMessageEx> builder = new LivePagedListBuilder<>(
db.message().pagedFolder(folder, sort, false, debug), config);
builder.setBoundaryCallback(searchCallback);
messages = builder.build();
break;
case THREAD:
messages = new LivePagedListBuilder<>(db.message().pagedThread(thread, sort, debug), MESSAGES_PAGE_SIZE).build();
break;
}
} else {
if (searchCallback == null)
searchCallback = new BoundaryCallbackMessages(
getContext(), FragmentMessages.this,
folder, search,
folder, search, SEARCH_PAGE_SIZE,
new BoundaryCallbackMessages.IBoundaryCallbackMessages() {
@Override
public void onLoading() {
@ -718,79 +718,49 @@ public class FragmentMessages extends FragmentEx {
@Override
public void onError(Context context, Throwable ex) {
pbWait.setVisibility(View.GONE);
Helper.unexpectedError(context, ex);
}
});
Bundle args = new Bundle();
args.putLong("folder", folder);
args.putString("search", search);
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
if (searchState == SearchState.Reset) {
long folder = args.getLong("folder");
DB.getInstance(context).message().resetFound(folder);
searchState = SearchState.Database;
Log.i(Helper.TAG, "Search reset done");
}
return null;
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(SEARCH_PAGE_SIZE)
.setPrefetchDistance(SEARCH_PAGE_SIZE)
.build();
LivePagedListBuilder<Integer, TupleMessageEx> builder = new LivePagedListBuilder<>(
db.message().pagedFolder(folder, "time", true, false), config);
builder.setBoundaryCallback(searchCallback);
messages = builder.build();
}
@Override
protected void onLoaded(final Bundle args, Void data) {
LivePagedListBuilder<Integer, TupleMessageEx> builder = new LivePagedListBuilder<>(db.message().pagedFolder(folder, "time", true, false), SEARCH_PAGE_SIZE);
builder.setBoundaryCallback(searchCallback);
LiveData<PagedList<TupleMessageEx>> messages = builder.build();
messages.observe(getViewLifecycleOwner(), new Observer<PagedList<TupleMessageEx>>() {
@Override
public void onChanged(PagedList<TupleMessageEx> messages) {
Log.i(Helper.TAG, "Submit found messages=" + messages.size());
adapter.submitList(messages);
grpReady.setVisibility(View.VISIBLE);
public void onChanged(@Nullable PagedList<TupleMessageEx> messages) {
if (messages == null ||
(viewType == AdapterMessage.ViewType.THREAD && messages.size() == 0)) {
finish();
return;
}
});
new SimpleTask<Long>() {
@Override
protected Long onLoad(Context context, Bundle args) throws Throwable {
long last = 0;
if (searchState == SearchState.Database) {
last = new Date().getTime();
long folder = args.getLong("folder");
String search = args.getString("search").toLowerCase();
DB db = DB.getInstance(context);
for (long id : db.message().getMessageIDs(folder)) {
EntityMessage message = db.message().getMessage(id);
if (message != null) { // Message could be removed in the meantime
String from = MessageHelper.getFormattedAddresses(message.from, true);
if (from.toLowerCase().contains(search) ||
message.subject.toLowerCase().contains(search) ||
message.read(context).toLowerCase().contains(search)) {
Log.i(Helper.TAG, "Search found id=" + id);
db.message().setMessageFound(message.id, true);
last = message.received;
}
}
}
searchState = SearchState.Boundary;
Log.i(Helper.TAG, "Search database done");
}
return last;
}
Log.i(Helper.TAG, "Submit messages=" + messages.size());
adapter.submitList(messages);
@Override
protected void onLoaded(Bundle args, Long last) {
boolean searching = (searchCallback != null && searchCallback.isSearching());
if (!searching)
pbWait.setVisibility(View.GONE);
searchCallback.setEnabled(true);
if (last > 0)
searchCallback.load(last);
}
}.load(FragmentMessages.this, args);
grpReady.setVisibility(View.VISIBLE);
if (messages.size() == 0 && !searching) {
tvNoEmail.setVisibility(View.VISIBLE);
rvMessage.setVisibility(View.GONE);
} else {
tvNoEmail.setVisibility(View.GONE);
rvMessage.setVisibility(View.VISIBLE);
}
}.load(this, args);
}
});
}
void onNewMessages() {

@ -39,6 +39,7 @@ import android.widget.Spinner;
import com.android.billingclient.api.BillingClient;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.sun.mail.imap.IMAPStore;
import java.io.File;
import java.io.FileInputStream;
@ -53,7 +54,9 @@ import java.text.DecimalFormat;
import java.util.concurrent.ThreadFactory;
import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.FolderClosedException;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import androidx.appcompat.app.AlertDialog;
@ -214,7 +217,20 @@ public class Helper {
return filename.substring(index + 1);
}
static String refreshToken(Context context, String type, String name, String current) {
static void connect(Context context, IMAPStore istore, EntityAccount account) throws MessagingException {
try {
istore.connect(account.host, account.port, account.user, account.password);
} catch (AuthenticationFailedException ex) {
if (account.auth_type == Helper.AUTH_TYPE_GMAIL) {
account.password = Helper.refreshToken(context, "com.google", account.user, account.password);
DB.getInstance(context).account().setAccountPassword(account.id, account.password);
istore.connect(account.host, account.port, account.user, account.password);
} else
throw ex;
}
}
private static String refreshToken(Context context, String type, String name, String current) {
try {
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(type);
@ -313,7 +329,7 @@ public class Helper {
}
static boolean isPro(Context context) {
if (false && BuildConfig.DEBUG)
if (true && BuildConfig.DEBUG)
return true;
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("pro", false);
}

@ -94,11 +94,6 @@ public class JobDaily extends JobService {
int logs = db.log().deleteLogs(before);
Log.i(Helper.TAG, "Deleted logs=" + logs);
// Cleanup found messages
Log.i(Helper.TAG, "Cleanup found messages");
int found = db.message().deleteFoundMessages();
Log.i(Helper.TAG, "Deleted found messages=" + found);
Log.i(Helper.TAG, "End daily job");
}
});

@ -560,12 +560,6 @@ public class ServiceSynchronize extends LifecycleService {
boolean debug = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debug", false);
System.setProperty("mail.socket.debug", Boolean.toString(debug));
// Refresh token
if (account.auth_type == Helper.AUTH_TYPE_GMAIL) {
account.password = Helper.refreshToken(this, "com.google", account.user, account.password);
db.account().setAccountPassword(account.id, account.password);
}
// Create session
Properties props = MessageHelper.getSessionProperties(this, account.auth_type);
final Session isession = Session.getInstance(props, null);
@ -647,8 +641,7 @@ public class ServiceSynchronize extends LifecycleService {
for (EntityFolder folder : db.folder().getFolders(account.id))
db.folder().setFolderState(folder.id, null);
db.account().setAccountState(account.id, "connecting");
istore.connect(account.host, account.port, account.user, account.password);
Helper.connect(this, istore, account);
db.account().setAccountState(account.id, "connected");
db.account().setAccountError(account.id, null);
@ -704,7 +697,14 @@ public class ServiceSynchronize extends LifecycleService {
for (Message imessage : e.getMessages())
try {
long id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, false);
long id;
try {
db.beginTransaction();
id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
@ -777,7 +777,14 @@ public class ServiceSynchronize extends LifecycleService {
fp.add(IMAPFolder.FetchProfileItem.FLAGS);
ifolder.fetch(new Message[]{e.getMessage()}, fp);
long id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), false);
long id;
try {
db.beginTransaction();
id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), id);
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
@ -1235,12 +1242,6 @@ public class ServiceSynchronize extends LifecycleService {
return;
}
// Refresh token
if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
ident.password = Helper.refreshToken(this, "com.google", ident.user, ident.password);
db.identity().setIdentityPassword(ident.id, ident.password);
}
// Create session
Properties props = MessageHelper.getSessionProperties(this, ident.auth_type);
final Session isession = Session.getInstance(props, null);
@ -1518,11 +1519,13 @@ public class ServiceSynchronize extends LifecycleService {
long headers = SystemClock.elapsedRealtime();
ifolder.fetch(full.toArray(new Message[0]), fp);
Log.i(Helper.TAG, folder.name + " fetched headers=" + full.size() +
" " + (SystemClock.elapsedRealtime() - fetch) + " ms");
" " + (SystemClock.elapsedRealtime() - headers) + " ms");
for (int j = isub.length - 1; j >= 0; j--)
try {
db.beginTransaction();
ids[from + j] = synchronizeMessage(this, folder, ifolder, (IMAPMessage) isub[j], false);
db.setTransactionSuccessful();
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} catch (FolderClosedException ex) {
@ -1532,6 +1535,7 @@ public class ServiceSynchronize extends LifecycleService {
} catch (Throwable ex) {
Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
} finally {
db.endTransaction();
// Reduce memory usage
((IMAPMessage) isub[j]).invalidateHeaders();
}
@ -1590,8 +1594,6 @@ public class ServiceSynchronize extends LifecycleService {
boolean flagged = helper.getFlagged();
DB db = DB.getInstance(context);
try {
db.beginTransaction();
// Find message by uid (fast, no headers required)
EntityMessage message = db.message().getMessageByUid(folder.id, uid);
@ -1733,12 +1735,7 @@ public class ServiceSynchronize extends LifecycleService {
}
}
db.setTransactionSuccessful();
return message.id;
} finally {
db.endTransaction();
}
}
private static void downloadMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, long id) throws MessagingException, IOException {

@ -215,7 +215,7 @@
<string name="title_queued">Sending message</string>
<string name="title_search">Search</string>
<string name="title_search_hint">Search sender/subject/text</string>
<string name="title_search_hint">Search on server</string>
<string name="title_searching">Searching \'%1$s\'</string>
<string name="title_sort_on">Sort on</string>

Loading…
Cancel
Save