diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index d0da46f0bf..b5f82b9674 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -52,7 +52,6 @@ import java.util.List; import java.util.Locale; import javax.mail.Address; -import javax.mail.internet.InternetAddress; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -189,11 +188,9 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack protected Long onLoad(Context context, Bundle args) throws Throwable { File file = new File(context.getCacheDir(), "crash.log"); if (file.exists()) { - Address to = new InternetAddress("marcel+email@faircode.eu", "FairCode"); - // Get version info StringBuilder sb = new StringBuilder(); - sb.append(String.format("%s: %s/%d\r\n", BuildConfig.APPLICATION_ID, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); + sb.append(String.format("%s: %s\r\n", context.getString(R.string.app_name), BuildConfig.VERSION_NAME)); sb.append(String.format("Android: %s (SDK %d)\r\n", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); sb.append("\r\n"); @@ -213,7 +210,7 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack String line; in = new BufferedReader(new FileReader(file)); while ((line = in.readLine()) != null) - sb.append(line); + sb.append(line).append("\r\n"); } finally { if (in != null) in.close(); @@ -232,8 +229,8 @@ public class ActivityView extends ActivityBase implements FragmentManager.OnBack draft = new EntityMessage(); draft.account = drafts.account; draft.folder = drafts.id; - draft.msgid = draft.generateMessageId(); - draft.to = new Address[]{to}; + draft.msgid = EntityMessage.generateMessageId(); + draft.to = new Address[]{Helper.myAddress()}; draft.subject = context.getString(R.string.app_name) + " crash log"; draft.body = "
" + sb.toString().replaceAll("\\r?\\n", ""; draft.received = new Date().getTime(); diff --git a/app/src/main/java/eu/faircode/email/ApplicationEx.java b/app/src/main/java/eu/faircode/email/ApplicationEx.java index f67bfc92af..e3ada1d3e2 100644 --- a/app/src/main/java/eu/faircode/email/ApplicationEx.java +++ b/app/src/main/java/eu/faircode/email/ApplicationEx.java @@ -20,6 +20,9 @@ package eu.faircode.email; */ import android.app.Application; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.util.Log; import java.io.File; @@ -63,5 +66,28 @@ public class ApplicationEx extends Application { prev.uncaughtException(thread, ex); } }); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + NotificationManager nm = getSystemService(NotificationManager.class); + + NotificationChannel service = new NotificationChannel( + "service", + getString(R.string.channel_service), + NotificationManager.IMPORTANCE_MIN); + service.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT); + nm.createNotificationChannel(service); + + NotificationChannel notification = new NotificationChannel( + "notification", + getString(R.string.channel_notification), + NotificationManager.IMPORTANCE_DEFAULT); + nm.createNotificationChannel(notification); + + NotificationChannel error = new NotificationChannel( + "error", + getString(R.string.channel_error), + NotificationManager.IMPORTANCE_HIGH); + nm.createNotificationChannel(error); + } } } diff --git a/app/src/main/java/eu/faircode/email/FragmentAbout.java b/app/src/main/java/eu/faircode/email/FragmentAbout.java index 7146ea6c8f..a36a13c1e8 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAbout.java +++ b/app/src/main/java/eu/faircode/email/FragmentAbout.java @@ -21,6 +21,7 @@ package eu.faircode.email; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -33,7 +34,6 @@ import java.io.UnsupportedEncodingException; import java.util.Date; import javax.mail.Address; -import javax.mail.internet.InternetAddress; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -58,10 +58,27 @@ public class FragmentAbout extends FragmentEx { new SimpleTask
") + "
" + info.toString().replaceAll("\\r?\\n", ""; + draft.msgid = EntityMessage.generateMessageId(); + draft.to = new Address[]{Helper.myAddress()}; + draft.subject = context.getString(R.string.app_name) + " debug info"; + draft.body = "
") + "
" + sb.toString().replaceAll("\\r?\\n", ""; draft.received = new Date().getTime(); draft.seen = false; draft.ui_seen = false; diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index e815274f8c..337ba699ed 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -565,7 +565,7 @@ public class FragmentCompose extends FragmentEx { draft = new EntityMessage(); draft.account = account; draft.folder = drafts.id; - draft.msgid = draft.generateMessageId(); // for multiple appends + draft.msgid = EntityMessage.generateMessageId(); // for multiple appends if (ref != null) { draft.thread = ref.thread; diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index 6591cc6393..0e53b484a2 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -21,7 +21,6 @@ package eu.faircode.email; import android.content.Context; import android.content.res.TypedArray; -import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.view.Menu; @@ -38,9 +37,13 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; + +import javax.mail.Address; +import javax.mail.internet.InternetAddress; public class Helper { - static final String TAG = BuildConfig.APPLICATION_ID; + static final String TAG = "fairemail"; static int resolveColor(Context context, int attr) { int[] attrs = new int[]{attr}; @@ -102,37 +105,20 @@ public class Helper { return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } - static StringBuilder getDebugInfo() { + static StringBuilder getLogcat() { StringBuilder sb = new StringBuilder(); - // Get version info - sb.append(String.format("%s: %s/%d\r\n", BuildConfig.APPLICATION_ID, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - sb.append(String.format("Android: %s (SDK %d)\r\n", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); - sb.append("\r\n"); - - // Get device info - sb.append(String.format("Brand: %s\r\n", Build.BRAND)); - sb.append(String.format("Manufacturer: %s\r\n", Build.MANUFACTURER)); - sb.append(String.format("Model: %s\r\n", Build.MODEL)); - sb.append(String.format("Product: %s\r\n", Build.PRODUCT)); - sb.append(String.format("Device: %s\r\n", Build.DEVICE)); - sb.append(String.format("Host: %s\r\n", Build.HOST)); - sb.append(String.format("Display: %s\r\n", Build.DISPLAY)); - sb.append(String.format("Id: %s\r\n", Build.ID)); - sb.append("\r\n"); - - // Get logcat Process proc = null; BufferedReader br = null; try { - String[] cmd = new String[]{"logcat", "-d", "-v", "threadtime"}; + String[] cmd = new String[]{"logcat", "-d", "-v", "threadtime", TAG + ":I"}; proc = Runtime.getRuntime().exec(cmd); br = new BufferedReader(new InputStreamReader(proc.getInputStream())); String line; while ((line = br.readLine()) != null) sb.append(line).append("\r\n"); } catch (IOException ex) { - Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + Log.e(TAG, ex + "\n" + Log.getStackTraceString(ex)); } finally { if (br != null) try { @@ -143,10 +129,14 @@ public class Helper { try { proc.destroy(); } catch (Throwable ex) { - Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + Log.w(TAG, ex + "\n" + Log.getStackTraceString(ex)); } } return sb; } + + static Address myAddress() throws UnsupportedEncodingException { + return new InternetAddress("marcel+fairemail@faircode.eu", "FairCode"); + } } diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index deff49a6be..731f571e8e 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -63,10 +63,11 @@ public class MessageHelper { props.put("mail.imaps.ssl.checkserveridentity", "true"); props.put("mail.imaps.ssl.trust", "*"); props.put("mail.imaps.starttls.enable", "false"); - props.put("mail.imaps.timeout", "20000"); + props.put("mail.imaps.connectiontimeout", "20000"); + props.put("mail.imaps.timeout", "20000"); + props.put("mail.imaps.writetimeout", "20000"); // one thread overhead - // https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties props.put("mail.smtps.ssl.checkserveridentity", "true"); props.put("mail.smtps.ssl.trust", "*"); props.put("mail.smtps.starttls.enable", "false"); diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index 6e8dc5bf60..e961a15286 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -20,7 +20,6 @@ package eu.faircode.email; */ import android.app.Notification; -import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; @@ -33,6 +32,7 @@ import android.media.RingtoneManager; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.Uri; import android.os.Binder; @@ -318,9 +318,11 @@ public class ServiceSynchronize extends LifecycleService { } private void reportError(String account, String folder, Throwable ex) { - String action = account + "/" + folder; - NotificationManager nm = getSystemService(NotificationManager.class); - nm.notify(action, 1, getNotificationError(action, ex).build()); + if (!(ex instanceof FolderClosedException) && !(ex instanceof IllegalStateException)) { + String action = account + "/" + folder; + NotificationManager nm = getSystemService(NotificationManager.class); + nm.notify(action, 1, getNotificationError(action, ex).build()); + } } private void monitorAccount(final EntityAccount account, final ServiceState state) { @@ -334,7 +336,6 @@ public class ServiceSynchronize extends LifecycleService { int backoff = CONNECT_BACKOFF_START; while (state.running) { IMAPStore istore = null; - final Semaphore semaphore = new Semaphore(0, true); try { Properties props = MessageHelper.getSessionProperties(); props.setProperty("mail.imaps.peek", "true"); @@ -416,14 +417,6 @@ public class ServiceSynchronize extends LifecycleService { monitorFolder(account, folder, fstore, ifolder, state); - } catch (FolderClosedException ex) { - // Happens when no connectivity - Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); - - } catch (IllegalStateException ex) { - // Happens when syncing message - // This operation is not allowed on a closed folder - Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } catch (Throwable ex) { // MessagingException // - message: connection failure @@ -435,13 +428,20 @@ public class ServiceSynchronize extends LifecycleService { db.folder().setFolderError(folder.id, Helper.formatThrowable(ex)); + // FolderClosedException: can happen when no connectivity + + // IllegalStateException: + // - "This operation is not allowed on a closed folder" + // - can happen when syncing message + // Cascade up - if (connected) - try { - fstore.close(); - } catch (MessagingException e1) { - Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1)); - } + if (!(ex instanceof FolderClosedException) && !(ex instanceof IllegalStateException)) + if (connected) + try { + fstore.close(); + } catch (MessagingException e1) { + Log.w(Helper.TAG, account.name + " " + e1 + "\n" + Log.getStackTraceString(e1)); + } } finally { if (ifolder != null && ifolder.isOpen()) { try { @@ -507,8 +507,6 @@ public class ServiceSynchronize extends LifecycleService { synchronized (state) { state.notifyAll(); } - - semaphore.release(); } @Override @@ -528,8 +526,6 @@ public class ServiceSynchronize extends LifecycleService { synchronized (state) { state.notifyAll(); } - - semaphore.release(); } private BroadcastReceiver processReceiver = new BroadcastReceiver() { @@ -544,7 +540,7 @@ public class ServiceSynchronize extends LifecycleService { final boolean shouldClose = (ifolder == null); final IMAPFolder ffolder = ifolder; - Log.i(Helper.TAG, "run operations folder=" + fid + " offline=" + shouldClose); + Log.v(Helper.TAG, "run operations folder=" + fid + " offline=" + shouldClose); try { executor.submit(new Runnable() { @Override @@ -553,7 +549,7 @@ public class ServiceSynchronize extends LifecycleService { EntityFolder folder = db.folder().getFolder(fid); IMAPFolder ifolder = ffolder; try { - Log.i(Helper.TAG, folder.name + " start operations"); + Log.v(Helper.TAG, folder.name + " start operations"); if (ifolder == null) { // Prevent unnecessary folder connections @@ -577,7 +573,7 @@ public class ServiceSynchronize extends LifecycleService { Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } } - Log.i(Helper.TAG, folder.name + " stop operations"); + Log.v(Helper.TAG, folder.name + " stop operations"); } } }); @@ -626,21 +622,13 @@ public class ServiceSynchronize extends LifecycleService { Log.i(Helper.TAG, account.name + " closing"); if (istore != null) { try { + // This can take 20 seconds istore.close(); } catch (MessagingException ex) { Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex)); } } - Log.i(Helper.TAG, account.name + " waiting for close"); - boolean acquired = false; - while (!acquired) - try { - semaphore.acquire(); - acquired = true; - } catch (InterruptedException ex) { - Log.e(Helper.TAG, account.name + " acquire " + ex.getMessage()); - } - Log.i(Helper.TAG, account.name + " reported closed"); + Log.i(Helper.TAG, account.name + " closed"); } if (state.running) { @@ -658,18 +646,8 @@ public class ServiceSynchronize extends LifecycleService { db.account().setAccountState(account.id, null); - for (Thread t : threads) { - boolean joined = false; - while (!joined) - try { - Log.i(Helper.TAG, "Joining " + t.getName()); - t.join(); - joined = true; - Log.i(Helper.TAG, "Joined " + t.getName()); - } catch (InterruptedException ex) { - Log.w(Helper.TAG, t.getName() + " join " + ex.toString()); - } - } + for (Thread t : threads) + join(t); threads.clear(); executor.shutdown(); @@ -1107,7 +1085,7 @@ public class ServiceSynchronize extends LifecycleService { private void synchronizeFolders(EntityAccount account, IMAPStore istore) throws MessagingException { try { - Log.i(Helper.TAG, "Start sync folders"); + Log.v(Helper.TAG, "Start sync folders"); DB db = DB.getInstance(this); @@ -1155,13 +1133,13 @@ public class ServiceSynchronize extends LifecycleService { for (String name : names) db.folder().deleteFolder(account.id, name); } finally { - Log.i(Helper.TAG, "End sync folder"); + Log.v(Helper.TAG, "End sync folder"); } } private void synchronizeMessages(EntityFolder folder, IMAPFolder ifolder) throws MessagingException, JSONException, IOException { try { - Log.i(Helper.TAG, folder.name + " start sync after=" + folder.after); + Log.v(Helper.TAG, folder.name + " start sync after=" + folder.after); DB db = DB.getInstance(this); @@ -1232,7 +1210,7 @@ public class ServiceSynchronize extends LifecycleService { Log.w(Helper.TAG, folder.name + " statistics added=" + added + " updated=" + updated + " unchanged=" + unchanged); } finally { - Log.i(Helper.TAG, folder.name + " end sync"); + Log.v(Helper.TAG, folder.name + " end sync"); } } @@ -1377,15 +1355,53 @@ public class ServiceSynchronize extends LifecycleService { } private class ServiceManager extends ConnectivityManager.NetworkCallback { - ServiceState state = new ServiceState(); + private ServiceState state = new ServiceState(); + private boolean connected = false; private Thread main; private EntityFolder outbox = null; + private ExecutorService lifecycle = Executors.newSingleThreadExecutor(); private ExecutorService executor = Executors.newSingleThreadExecutor(); @Override public void onAvailable(Network network) { - Log.i(Helper.TAG, "Available " + network); + Log.i(Helper.TAG, "Network available " + network); + + if (!connected) { + Log.i(Helper.TAG, "Network not connected"); + connected = true; + lifecycle.submit(new Runnable() { + @Override + public void run() { + start(); + } + }); + } + } + @Override + public void onLost(Network network) { + Log.i(Helper.TAG, "Network lost " + network); + + if (connected) { + Log.i(Helper.TAG, "Network connected"); + ConnectivityManager cm = getSystemService(ConnectivityManager.class); + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni != null) + Log.i(Helper.TAG, "Network active=" + ni); + if (ni == null || !ni.isConnected()) { + Log.i(Helper.TAG, "Network disconnected=" + ni); + connected = false; + lifecycle.submit(new Runnable() { + @Override + public void run() { + stop(); + } + }); + } + } + } + + public void start() { synchronized (state) { state.running = true; } @@ -1442,18 +1458,8 @@ public class ServiceSynchronize extends LifecycleService { } // Stop monitoring accounts - for (Thread t : threads) { - boolean joined = false; - while (!joined) - try { - Log.i(Helper.TAG, "Joining " + t.getName()); - t.join(); - joined = true; - Log.i(Helper.TAG, "Joined " + t.getName()); - } catch (InterruptedException ex) { - Log.w(Helper.TAG, t.getName() + " join " + ex.toString()); - } - } + for (Thread t : threads) + join(t); threads.clear(); executor.shutdown(); @@ -1470,19 +1476,6 @@ public class ServiceSynchronize extends LifecycleService { main.start(); } - @Override - public void onLost(Network network) { - Log.i(Helper.TAG, "Lost " + network); - - if (main != null) - new Thread(new Runnable() { - @Override - public void run() { - stop(); - } - }).start(); - } - public void stop() { if (main != null) { synchronized (state) { @@ -1491,17 +1484,7 @@ public class ServiceSynchronize extends LifecycleService { } main.interrupt(); // stop backoff - - boolean joined = false; - while (!joined) - try { - Log.i(Helper.TAG, "Joining " + main.getName()); - main.join(); - joined = true; - Log.i(Helper.TAG, "Joined " + main.getName()); - } catch (InterruptedException ex) { - Log.e(Helper.TAG, main.getName() + " join " + ex.toString()); - } + join(main); main = null; } @@ -1510,7 +1493,7 @@ public class ServiceSynchronize extends LifecycleService { private BroadcastReceiver outboxReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Log.i(Helper.TAG, outbox.name + " run operations"); + Log.v(Helper.TAG, outbox.name + " run operations"); // Create session Properties props = MessageHelper.getSessionProperties(); @@ -1521,13 +1504,13 @@ public class ServiceSynchronize extends LifecycleService { @Override public void run() { try { - Log.i(Helper.TAG, outbox.name + " start operations"); + Log.v(Helper.TAG, outbox.name + " start operations"); processOperations(outbox, isession, null, null); } catch (Throwable ex) { Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex)); reportError(null, outbox.name, ex); } finally { - Log.i(Helper.TAG, outbox.name + " end operations"); + Log.v(Helper.TAG, outbox.name + " end operations"); } } }); @@ -1538,31 +1521,28 @@ public class ServiceSynchronize extends LifecycleService { }; } - public static void start(Context context) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - NotificationManager nm = context.getSystemService(NotificationManager.class); - - NotificationChannel service = new NotificationChannel( - "service", - context.getString(R.string.channel_service), - NotificationManager.IMPORTANCE_MIN); - service.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT); - nm.createNotificationChannel(service); - - NotificationChannel notification = new NotificationChannel( - "notification", - context.getString(R.string.channel_notification), - NotificationManager.IMPORTANCE_DEFAULT); - nm.createNotificationChannel(notification); - - NotificationChannel error = new NotificationChannel( - "error", - context.getString(R.string.channel_error), - NotificationManager.IMPORTANCE_HIGH); - nm.createNotificationChannel(error); - } + private static void join(Thread thread) { + boolean joined = false; + while (!joined) + try { + Log.i(Helper.TAG, "Joining " + thread.getName()); + thread.join(); + joined = true; + Log.i(Helper.TAG, "Joined " + thread.getName()); + } catch (InterruptedException ex) { + Log.e(Helper.TAG, thread.getName() + " join " + ex.toString()); + } + } - ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class)); + private static void acquire(Semaphore semaphore, String name) { + boolean acquired = false; + while (!acquired) + try { + semaphore.acquire(); + acquired = true; + } catch (InterruptedException ex) { + Log.e(Helper.TAG, name + " acquire " + ex.toString()); + } } private IBinder binder = new LocalBinder(); @@ -1585,6 +1565,10 @@ public class ServiceSynchronize extends LifecycleService { stopSelf(); } + public static void start(Context context) { + ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class)); + } + public static void stopSynchronous(Context context, String reason) { Log.i(Helper.TAG, "Stop because of '" + reason + "'"); @@ -1616,15 +1600,7 @@ public class ServiceSynchronize extends LifecycleService { if (exists) { Log.i(Helper.TAG, "Service stopping"); - boolean acquired = false; - while (!acquired) - try { - semaphore.acquire(); - acquired = true; - } catch (InterruptedException ex) { - Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex)); - } - + acquire(semaphore, "service"); context.getApplicationContext().unbindService(connection); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f7ab61e15..87b7d2f17c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,9 +32,9 @@
") + "