Compare commits

...

91 Commits
1.74 ... 1.85

Author SHA1 Message Date
M66B
6927650e78 1.85 release 2018-10-12 13:58:40 +00:00
M66B
0246aab0bf Crowdin sync 2018-10-12 13:55:07 +00:00
M66B
4eb1988427 Move somewhere 2018-10-12 13:53:57 +00:00
M66B
2cc3c07651 Android 9 Pie compatibility 2018-10-12 11:41:31 +00:00
M66B
f351bccb8e 1.84 release 2018-10-12 08:33:23 +00:00
M66B
ce57552886 Fixed crash on attachment in some cases 2018-10-12 08:28:48 +00:00
M66B
350fbdf486 Prevent new message notifications on first sync 2018-10-12 07:48:31 +00:00
M66B
b84a4ded3c Small layout improvement 2018-10-12 06:33:43 +00:00
M66B
38171d08ad Crowdin sync 2018-10-12 06:03:34 +00:00
M66B
d9476a291a Updated gradle 2018-10-12 05:59:54 +00:00
M66B
781201ad64 Revert "Hide doze setup"
This reverts commit 2f702d6a64.
2018-10-12 05:55:35 +00:00
M66B
5a356ab23c Log wakelocks 2018-10-11 15:43:09 +00:00
M66B
f731535e3c 1.83 release 2018-10-11 12:24:06 +00:00
M66B
7810193a80 Crowdin sync 2018-10-11 12:21:37 +00:00
M66B
4c3d158d82 Fixed wakelock 2018-10-11 12:20:27 +00:00
M66B
f26b915ece Updated FAQ 2018-10-11 12:04:41 +00:00
M66B
2fa0d298cf Added main enable/disable in advanced settings 2018-10-11 11:59:56 +00:00
M66B
74349ed1cf 1.82 release 2018-10-11 09:16:57 +00:00
M66B
6b3821c82c Crowdin sync 2018-10-11 09:15:22 +00:00
M66B
926ac6cf0a Added option to enlarge text size in distraction free mode 2018-10-11 09:14:01 +00:00
M66B
43d7add8f4 Remove underline spans 2018-10-11 07:47:21 +00:00
M66B
828ba46bed 1.81 release 2018-10-11 06:15:08 +00:00
M66B
2f702d6a64 Hide doze setup 2018-10-11 06:13:27 +00:00
M66B
ebeccf71d8 Crowdin sync 2018-10-11 06:10:41 +00:00
M66B
1071688459 Set attachment title when sharing 2018-10-11 06:07:47 +00:00
M66B
6789ad9c3c Handle existing found messages 2018-10-11 05:48:44 +00:00
M66B
e602b27fa3 Revert "Close thread view on archive"
This reverts commit 643770b139.
2018-10-10 16:57:50 +00:00
M66B
5466e06b07 Revert "Increase default keep alive interval to 19 minutes"
This reverts commit 362f025267.
2018-10-10 16:15:53 +00:00
M66B
362f025267 Increase default keep alive interval to 19 minutes 2018-10-10 16:13:26 +00:00
M66B
97678efb6b Updated compatibility section 2018-10-10 15:44:29 +00:00
M66B
a3341db947 Updated FAQ 2018-10-10 15:41:25 +00:00
M66B
4228a86431 Change default keep alive to 12 minutes 2018-10-10 14:29:41 +00:00
M66B
643770b139 Close thread view on archive 2018-10-10 14:29:22 +00:00
M66B
67b2f1ca5b Added invite 2018-10-10 13:45:50 +00:00
M66B
7f35a89413 Updated FAQ 2018-10-10 13:12:10 +00:00
M66B
cbc4bfc22e Cleanup 2018-10-10 13:01:29 +00:00
M66B
195acc4bf2 Updated libraries 2018-10-10 11:08:44 +00:00
M66B
35d996bc4d Added compatibility section 2018-10-10 09:23:24 +00:00
M66B
e4cb13b9ec Revert "Hide battery optimization setup"
This reverts commit 7a55f8b8ed.
2018-10-10 08:36:13 +00:00
M66B
9ecdfaafa4 Fixed stopping idlers 2018-10-10 08:07:37 +00:00
M66B
7a55f8b8ed Hide battery optimization setup 2018-10-10 08:00:59 +00:00
M66B
681a8a31e2 Yield wake lock 2018-10-10 08:00:42 +00:00
M66B
0d340d1865 1.80 release 2018-10-10 07:33:15 +00:00
M66B
155506ce81 Keep alive 2018-10-10 07:26:04 +00:00
M66B
bf310662c1 Reset search 2018-10-09 20:31:26 +00:00
M66B
2f0babc479 Use alarm manager to keep alive 2018-10-09 20:31:18 +00:00
M66B
b19f496ce0 Use timers 2018-10-08 06:24:30 +00:00
M66B
1140ae3bf8 1.79 release 2018-10-07 18:47:05 +00:00
M66B
b75088833e Fixed synchronization 2018-10-07 18:43:44 +00:00
M66B
8f784c8954 Add keep alive wait to log 2018-10-07 17:34:59 +00:00
M66B
70fbc5aa6a 1.78 release 2018-10-07 15:49:34 +00:00
M66B
095a885d30 Fixed checking folder connection 2018-10-07 15:44:05 +00:00
M66B
38df2f6d24 Fixed null errors 2018-10-07 15:43:55 +00:00
M66B
3a53d69b31 Added FAQ 2018-10-07 12:58:09 +00:00
M66B
215fbbb74e Small improvement 2018-10-07 12:31:29 +00:00
M66B
b867e93291 Prevent crash 2018-10-07 12:21:55 +00:00
M66B
e496278128 Updated FAQ 2018-10-07 11:14:41 +00:00
M66B
e7292f0fc9 1.77 release 2018-10-07 10:55:15 +00:00
M66B
f4a25a359d Crowdin sync 2018-10-07 10:52:46 +00:00
M66B
ee4339a971 Simplify store keep alive 2018-10-07 10:19:38 +00:00
M66B
0bf88fcf9e Small fix/improvement 2018-10-07 09:49:22 +00:00
M66B
11d355c3d5 Refactoring 2018-10-07 08:45:38 +00:00
M66B
ed881a42ad Refactoring 2018-10-07 08:08:01 +00:00
M66B
1fe8784b1f Small improvements/fixes 2018-10-07 06:53:09 +00:00
M66B
5fe713180d Make folder serializable 2018-10-06 21:49:46 +00:00
M66B
c0fa6acc33 Fixed folder compare 2018-10-06 21:42:20 +00:00
M66B
09b530dd9d Fixed folder export/import 2018-10-06 21:36:11 +00:00
M66B
3f0aa1b59a Fixed empty reply message body 2018-10-06 21:31:12 +00:00
M66B
a4949c6473 Fixed search on server for messages already stored locally 2018-10-06 21:23:06 +00:00
M66B
64493bb573 Search on to address 2018-10-06 21:06:39 +00:00
M66B
6bb0d05478 Show badges for new messages notification only 2018-10-06 20:58:32 +00:00
M66B
d3fc9aadcc Prevent crash when storage access framework not installed 2018-10-06 20:55:47 +00:00
M66B
d7c4064f0b Workaround bug in Sony Android version 2018-10-06 20:55:06 +00:00
M66B
7a47f40fe1 Prevent crash 2018-10-06 20:30:13 +00:00
M66B
a2ab19206c Fixed commas in email addresses 2018-10-01 16:59:03 +00:00
M66B
faf36edef0 Updated FAQ 2018-10-01 15:41:04 +00:00
M66B
34884e7e85 Auto download attachments on unmetered connection 2018-10-01 13:05:14 +00:00
M66B
aa138cabc0 1.76 release 2018-10-01 09:22:31 +00:00
M66B
87817b9412 Crowdin sync 2018-10-01 09:16:04 +00:00
M66B
11e9333f14 Added option to set poll interval per folder 2018-10-01 09:14:21 +00:00
M66B
703d8af6e8 Fixed displaying stars in thread view 2018-10-01 08:32:58 +00:00
M66B
0c2b035970 1.75 release 2018-10-01 05:47:19 +00:00
M66B
ad218d7b49 Crowdin sync 2018-10-01 05:44:51 +00:00
M66B
a0fc3c4058 Report Play store crashes too 2018-09-30 15:43:16 +00:00
M66B
695bb5bc8a Enable log/debug info in release builds again 2018-09-30 15:33:57 +00:00
M66B
6786358082 Fixed potential crash 2018-09-30 15:23:17 +00:00
M66B
f93e4ca813 Fixed out of memory on viewing large original messages 2018-09-30 15:20:30 +00:00
M66B
4fb2e7dbc2 Removed limit of 25 folders to synchronize 2018-09-30 15:02:29 +00:00
M66B
84eeadd2f7 Poll user folders 2018-09-30 14:55:47 +00:00
M66B
e41cc0cccf Fixed race condition 2018-09-30 13:49:01 +00:00
M66B
4d5d202ed7 Updated legend 2018-09-30 11:11:30 +00:00
151 changed files with 2451 additions and 948 deletions

Binary file not shown.

47
FAQ.md
View File

@@ -8,11 +8,12 @@ At the bottom you can find how to ask other questions, request features and repo
<a name="FAQ1"></a>
**(1) Which permissions are needed and why?**
* Full network access (INTERNET): to send and receive email
* View network connections (ACCESS_NETWORK_STATE): to monitor internet connectivity changes
* Run at startup (RECEIVE_BOOT_COMPLETED): to start monitoring on device start
* In-app billing (BILLING): to allow in-app purchases
* Foreground service (FOREGROUND_SERVICE): to run a foreground service on Android 9 Pie and later, see also the next question
* have full network access (INTERNET): to send and receive email
* view network connections (ACCESS_NETWORK_STATE): to monitor internet connectivity changes
* run at startup (RECEIVE_BOOT_COMPLETED): to start monitoring on device start
* in-app billing (BILLING): to allow in-app purchases
* foreground service (FOREGROUND_SERVICE): to run a foreground service on Android 9 Pie and later, see also the next question
* prevent device from sleeping (WAKE_LOCK): to keep the device awake while synchronizing messages
* Optional: read your contacts (READ_CONTACTS): to autocomplete addresses and to show photos
* Optional: find accounts on the device (GET_ACCOUNTS): to use [OAuth](https://en.wikipedia.org/wiki/OAuth) instead of passwords
@@ -24,6 +25,8 @@ to prevent Android from killing the service that takes care of receiving and sen
Most, if not all, other email apps don't show a notification with the "side effect" that new email is often not or late being reported.
Background: this is because of the introduction of [doze mode](https://developer.android.com/training/monitoring-device-state/doze-standby) in Android 6 Marshmallow.
<a name="FAQ3"></a>
**(3) What are operations and why are they pending?**
@@ -99,18 +102,21 @@ So, unless your provider can enable this extension, you cannot use FairEmail for
**(11) Why is STARTTLS for IMAP not supported?**
STARTTLS starts with a not encrypted connection and is therefore not secure.
All known IMAP servers support IMAP with STARTTLS, so there is no need to support STARTTLS for IMAP.
If you encounter an IMAP server that requires STARTTLS, please [create an issue](https://github.com/M66B/open-source-email/issues/new).
All well known IMAP servers support IMAP with a plain SSL connection, so there is no need to support STARTTLS for IMAP.
If you encounter an IMAP server that requires STARTTLS, please let me know.
For more background information, please see [this article](https://www.eff.org/nl/deeplinks/2018/06/announcing-starttls-everywhere-securing-hop-hop-email-delivery).
tl;dr; "*Additionally, even if you configure STARTTLS perfectly and use a valid certificate, theres still no guarantee your communication will be encrypted.*"
<a name="FAQ13"></a>
**(13) How does progressive search work?**
**(13) How does search on server work?**
You can start searching for messages on sender, subject or text by using the magnify glass in the action bar of a folder.
First messages are searched on device, then the server is requested to search.
Scrolling down will download more messages from the server.
Searching on device is case insensitive and on partial text.
Searching on the server might be case sensitive or case insensitive and might be on partial text or whole words, depending on the provider.
Progressive search is a pro feature.
The server executes the search.
Scrolling down will fetch more messages from the server.
Searching by the server might be case sensitive or case insensitive and might be on partial text or whole words, depending on the provider.
Search on server is a pro feature.
<a name="FAQ14"></a>
**(14) How can I setup Outlook with 2FA?**
@@ -124,8 +130,11 @@ See [here](https://support.microsoft.com/en-us/help/12409/microsoft-account-app-
* More themes: the goal is to keep the app as simple as possible, so this will not be added.
* Encryption: there is too little interest in sending/receiving encrypted messages to justify putting effort into this.
* Multiple select: swiping is easier and doesn't have the risk of accidental touches, so multiple select would not add anything.
* Swipe left/right for previous/next message: this would be confusing since sometimes a message and sometimes a message conversation would be shown.
* Swipe left/right for previous/next message: this would be confusing since sometimes a message and sometimes a conversation would be shown.
* Open message from notification: this would be confusing since sometimes a message and sometimes a conversation would be opened.
* Preview message text: this is not always possible because the message text is initially not downloaded for large messages and besides that the subject is supposed to tell what the message is about.
* Filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for executing filter rules.
* Widget: FairEmail can be started from a shortcut and new messages are reported as status bar notifications, so I don't see what a widget would add.
<a name="FAQ16"></a>
**(16) Why are messages not being synchronized?**
@@ -227,6 +236,16 @@ Browse messages on the server will fetch messages from the email server in real
when you reach the end of the list of synchronized messages, even when the folder is set to not synchronize.
You can disable this feature under *Setup* > *Advanced options* > *Browse messages on the server*.
<a name="FAQ25"></a>
**(25) Why can't I select an image, attachment or a file to export/import?**
If a menu item to select a file is disabled (dimmed),
likely the [storage access framework](https://developer.android.com/guide/topics/providers/document-provider),
a standard Android component, is not present,
for example because your custom ROM does not include it or because it was removed.
FairEmail does not request storage permissions, so this framework is required to select files and folders.
No app, except maybe file managers, targetting Android 4.4 KitKat or later should ask for storage permissions because it would allow access to *all* files.
<br>
If you have another question, want to request a feature or report a bug, you can use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).

View File

@@ -70,6 +70,13 @@ This app starts a foreground service with a low priority status bar notification
* [GitHub](https://github.com/M66B/open-source-email/releases)
* [Play store](https://play.google.com/apps/testing/eu.faircode.email)
## Compatibility
FairEmail requires at least Android 6 Marshmallow.
FairEmail might occasionally crash on a Motorola/Lenovo Moto G4 and G5 with Android 7 Nougat or earlier
because of a [bug in Android](https://issuetracker.google.com/issues/63377371).
## Frequently asked questions
See [here](https://github.com/M66B/open-source-email/blob/master/FAQ.md) for a list of often asked questions.

View File

@@ -6,8 +6,8 @@ android {
applicationId "eu.faircode.email"
minSdkVersion 23
targetSdkVersion 28
versionCode 74
versionName "1.74"
versionCode 85
versionName "1.85"
archivesBaseName = "FairEmail-v$versionName"
javaCompileOptions {
@@ -42,6 +42,7 @@ android {
}
repositories {
google()
jcenter()
maven {
url "https://repo1.maven.org/maven2/"
@@ -73,13 +74,11 @@ dependencies {
// https://developer.android.com/topic/libraries/architecture/adding-components.html
// https://developer.android.com/jetpack/docs/release-notes
// https://github.com/open-keychain/openpgp-api
def androidx_version = "1.0.0"
def constraintlayout_version = "1.1.3"
def lifecycle_version = "2.0.0"
def room_version = "2.0.0-rc01"
def paging_version = "2.0.0-rc01"
def room_version = "2.0.0"
def paging_version = "2.0.0"
def billingclient_version = "1.1"
def javamail_version = "1.6.2"
def jsoup_version = "1.11.3"

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.vending.BILLING" />
<application

View File

@@ -92,6 +92,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
static final int REQUEST_ERROR = 3;
static final int REQUEST_ATTACHMENT = 1;
static final int REQUEST_INVITE = 2;
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE";
@@ -164,6 +165,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
case R.string.menu_rate:
onMenuRate();
break;
case R.string.menu_invite:
onMenuInvite();
break;
case R.string.menu_other:
onMenuOtherApps();
break;
@@ -222,6 +226,9 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
drawerArray.add(new DrawerItem(R.layout.item_drawer_separator));
if (getIntentInvite().resolveActivity(getPackageManager()) != null)
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_people_24, R.string.menu_invite));
if (getIntentRate().resolveActivity(getPackageManager()) != null)
drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_star_24, R.string.menu_rate));
@@ -567,6 +574,16 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
return intent;
}
private Intent getIntentInvite() {
Intent intent = new Intent("com.google.android.gms.appinvite.ACTION_APP_INVITE");
intent.setPackage("com.google.android.gms");
intent.putExtra("com.google.android.gms.appinvite.TITLE", getString(R.string.menu_invite));
intent.putExtra("com.google.android.gms.appinvite.MESSAGE", getString(R.string.title_try));
intent.putExtra("com.google.android.gms.appinvite.BUTTON_TEXT", getString(R.string.title_try));
// com.google.android.gms.appinvite.DEEP_LINK_URL
return intent;
}
private Intent getIntentOtherApps() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://play.google.com/store/apps/dev?id=8420080860664580239"));
@@ -652,6 +669,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
}
}
private void onMenuInvite() {
startActivityForResult(getIntentInvite(), REQUEST_INVITE);
}
private void onMenuOtherApps() {
Helper.view(this, getIntentOtherApps());
}

View File

@@ -26,6 +26,7 @@ import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -164,6 +165,8 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, attachment.type);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (!TextUtils.isEmpty(attachment.name))
intent.putExtra(Intent.EXTRA_TITLE, attachment.name);
Log.i(Helper.TAG, "Sharing " + file + " type=" + attachment.type);
Log.i(Helper.TAG, "Intent=" + intent);

View File

@@ -125,6 +125,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
}
private void clear() {
vwColor.setBackgroundColor(Color.TRANSPARENT);
ivFlagged.setVisibility(View.GONE);
ivAvatar.setVisibility(View.GONE);
tvFrom.setText(null);
@@ -156,9 +157,12 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
ivAvatar.setVisibility(photo ? View.VISIBLE : View.GONE);
vwColor.setBackgroundColor(message.accountColor == null ? Color.TRANSPARENT : message.accountColor);
vwColor.setVisibility(viewType == ViewType.UNIFIED ? View.VISIBLE : View.GONE);
vwColor.setVisibility(viewType == ViewType.UNIFIED && message.accountColor != null ? View.VISIBLE : View.GONE);
ivFlagged.setVisibility(message.count - message.unflagged > 0 ? View.VISIBLE : View.GONE);
if (viewType == ViewType.THREAD)
ivFlagged.setVisibility(message.unflagged == 1 ? View.GONE : View.VISIBLE);
else
ivFlagged.setVisibility(message.count - message.unflagged > 0 ? View.VISIBLE : View.GONE);
if (EntityFolder.DRAFTS.equals(message.folderType) ||
EntityFolder.OUTBOX.equals(message.folderType) ||

View File

@@ -67,6 +67,7 @@ public class ApplicationEx extends Application {
getString(R.string.channel_service),
NotificationManager.IMPORTANCE_MIN);
service.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
service.setShowBadge(false);
nm.createNotificationChannel(service);
NotificationChannel notification = new NotificationChannel(
@@ -79,13 +80,14 @@ public class ApplicationEx extends Application {
"error",
getString(R.string.channel_error),
NotificationManager.IMPORTANCE_HIGH);
error.setShowBadge(false);
nm.createNotificationChannel(error);
}
}
public boolean ownFault(Throwable ex) {
if (!Helper.isPlayStoreInstall(this))
return true;
//if (!Helper.isPlayStoreInstall(this))
// return true;
if (ex instanceof OutOfMemoryError)
return false;

View File

@@ -28,7 +28,9 @@ import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.util.FolderClosedIOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -43,6 +45,7 @@ import javax.mail.UIDFolder;
import javax.mail.search.BodyTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SubjectTerm;
import androidx.lifecycle.GenericLifecycleObserver;
@@ -62,6 +65,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private IMAPStore istore = null;
private IMAPFolder ifolder = null;
private Message[] imessages = null;
private List<Long> existing = new ArrayList<>();
private int index;
private boolean searching = false;
private int loaded = 0;
@@ -90,7 +94,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
@Override
public void run() {
Log.i(Helper.TAG, "Boundary close");
DB.getInstance(context).message().deleteFoundMessages();
DB db = DB.getInstance(context);
for (long id : existing)
db.message().setMessageFound(id, false);
db.message().deleteFoundMessages();
try {
if (istore != null)
istore.close();
@@ -101,6 +110,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
istore = null;
ifolder = null;
imessages = null;
existing.clear();
}
}
});
@@ -167,10 +177,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
else
imessages = ifolder.search(
new OrTerm(
new FromStringTerm(search),
new OrTerm(
new FromStringTerm(search),
new RecipientStringTerm(Message.RecipientType.TO, 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;
@@ -200,10 +216,14 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
try {
long uid = ifolder.getUID(isub[j]);
Log.i(Helper.TAG, "Boundary sync uid=" + uid);
if (db.message().getMessageByUid(fid, uid) == null) {
EntityMessage message = db.message().getMessageByUid(fid, uid);
if (message == null) {
ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) isub[j], search != null);
count++;
loaded++;
} else if (search != null) {
existing.add(message.id);
db.message().setMessageFound(message.id, true);
}
} catch (MessageRemovedException ex) {
Log.w(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));

View File

@@ -45,7 +45,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 19,
version = 20,
entities = {
EntityIdentity.class,
EntityAccount.class,
@@ -250,6 +250,13 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `folder` ADD COLUMN `hide` INTEGER NOT NULL DEFAULT 0");
}
})
.addMigrations(new Migration(19, 20) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `folder` ADD COLUMN `poll_interval` INTEGER");
}
})
.build();
}

View File

@@ -90,10 +90,6 @@ public interface DaoFolder {
" WHERE account = :account AND type = :type")
EntityFolder getFolderByType(long account, String type);
@Query("SELECT COUNT(folder.id) FROM folder" +
" WHERE account = :account AND `synchronize`")
int getFolderSyncCount(long account);
// For debug/crash info
@Query("SELECT folder.* FROM folder" +
" JOIN account ON account.id = folder.account" +

View File

@@ -139,24 +139,11 @@ public interface DaoMessage {
" OR message.msgid = :reference)")
List<EntityMessage> getMessageByMsgId(long account, String msgid, String reference);
@Query("SELECT message.* FROM message" +
" JOIN folder ON folder.id = message.folder" +
" WHERE message.account = :account" +
" AND message.thread = :thread" +
" AND folder.type <> '" + EntityFolder.OUTBOX + "'" +
" AND folder.type <> '" + EntityFolder.DRAFTS + "'")
List<EntityMessage> getMessageByThread(long account, String thread);
@Query("SELECT *" +
" FROM message" +
" WHERE folder = :folder")
List<EntityMessage> getMessageByFolder(long folder);
@Query("SELECT id FROM message" +
" WHERE folder = :folder" +
" ORDER BY message.received DESC, message.sent DESC")
List<Long> getMessageIDs(long folder);
@Query("SELECT message.*" +
", account.name AS accountName, account.color AS accountColor" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
@@ -224,9 +211,6 @@ public interface DaoMessage {
@Query("UPDATE message SET headers = :headers WHERE id = :id")
int setMessageHeaders(long id, String headers);
@Query("UPDATE message SET avatar = :avatar WHERE id = :id")
int setMessageAvatar(long id, String avatar);
@Query("DELETE FROM message WHERE id = :id")
int deleteMessage(long id);

View File

@@ -49,14 +49,14 @@ public class EntityAccount {
@NonNull
public Integer auth_type;
@NonNull
public Boolean primary;
@NonNull
public Boolean synchronize;
@NonNull
public Boolean primary;
public Integer color;
@NonNull
public Boolean store_sent; // obsolete
@NonNull
public Integer poll_interval; // NOOP interval
public Integer poll_interval; // keep-alive interval
public Long seen_until;
public String state;
public String error;
@@ -66,12 +66,15 @@ public class EntityAccount {
if (obj instanceof EntityAccount) {
EntityAccount other = (EntityAccount) obj;
return ((this.name == null ? other.name == null : this.name.equals(other.name)) &&
(this.signature == null ? other.signature == null : this.signature.equals(other.signature)) &&
this.host.equals(other.host) &&
this.port.equals(other.port) &&
this.user.equals(other.user) &&
this.password.equals(other.password) &&
this.primary.equals(other.primary) &&
this.auth_type.equals(other.auth_type) &&
this.synchronize.equals(other.synchronize) &&
this.primary.equals(other.primary) &&
(this.color == null ? other.color == null : this.color.equals(other.color)) &&
this.poll_interval.equals(other.poll_interval) &&
(this.seen_until == null ? other.seen_until == null : this.seen_until.equals(other.seen_until)) &&
(this.state == null ? other.state == null : this.state.equals(other.state)) &&
@@ -89,8 +92,8 @@ public class EntityAccount {
json.put("user", user);
json.put("password", "");
json.put("auth_type", auth_type);
json.put("primary", primary);
json.put("synchronize", false);
json.put("primary", primary);
if (color != null)
json.put("color", color);
json.put("poll_interval", poll_interval);
@@ -108,8 +111,8 @@ public class EntityAccount {
account.user = json.getString("user");
account.password = json.getString("password");
account.auth_type = json.getInt("auth_type");
account.primary = json.getBoolean("primary");
account.synchronize = json.getBoolean("synchronize");
account.primary = json.getBoolean("primary");
if (json.has("color"))
account.color = json.getInt("color");
account.poll_interval = json.getInt("poll_interval");

View File

@@ -19,12 +19,10 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.os.Parcel;
import android.os.Parcelable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
@@ -50,7 +48,7 @@ import static androidx.room.ForeignKey.CASCADE;
}
)
public class EntityFolder implements Parcelable {
public class EntityFolder implements Serializable {
static final String TABLE_NAME = "folder";
@PrimaryKey(autoGenerate = true)
@@ -62,6 +60,7 @@ public class EntityFolder implements Parcelable {
public String type;
@NonNull
public Boolean synchronize;
public Integer poll_interval; // obsolete
@NonNull
public Integer after; // days
public String display;
@@ -136,7 +135,7 @@ public class EntityFolder implements Parcelable {
this.name.equals(other.name) &&
this.type.equals(other.type) &&
this.synchronize.equals(other.synchronize) &&
this.after.equals(other.after) &&
(this.poll_interval == null ? other.poll_interval == null : this.poll_interval.equals(other.poll_interval)) && this.after.equals(other.after) &&
(this.display == null ? other.display == null : this.display.equals(other.display)) &&
this.hide == other.hide &&
this.unified == other.unified &&
@@ -151,81 +150,16 @@ public class EntityFolder implements Parcelable {
return name;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
if (id == null) {
parcel.writeByte((byte) 0);
} else {
parcel.writeByte((byte) 1);
parcel.writeLong(id);
}
if (account == null) {
parcel.writeByte((byte) 0);
} else {
parcel.writeByte((byte) 1);
parcel.writeLong(account);
}
parcel.writeString(name);
parcel.writeString(type);
parcel.writeByte((byte) (synchronize == null ? 0 : synchronize ? 1 : 2));
if (after == null) {
parcel.writeByte((byte) 0);
} else {
parcel.writeByte((byte) 1);
parcel.writeInt(after);
}
parcel.writeString(state);
parcel.writeString(error);
}
protected EntityFolder(Parcel in) {
if (in.readByte() == 0) {
id = null;
} else {
id = in.readLong();
}
if (in.readByte() == 0) {
account = null;
} else {
account = in.readLong();
}
name = in.readString();
type = in.readString();
byte tmpSynchronize = in.readByte();
synchronize = tmpSynchronize == 0 ? null : tmpSynchronize == 1;
if (in.readByte() == 0) {
after = null;
} else {
after = in.readInt();
}
state = in.readString();
error = in.readString();
}
public static final Creator<EntityFolder> CREATOR = new Creator<EntityFolder>() {
@Override
public EntityFolder createFromParcel(Parcel in) {
return new EntityFolder(in);
}
@Override
public EntityFolder[] newArray(int size) {
return new EntityFolder[size];
}
};
public JSONObject toJSON() throws JSONException {
JSONObject json = new JSONObject();
json.put("name", name);
json.put("type", type);
json.put("unified", unified);
json.put("synchronize", synchronize);
json.put("poll_interval", poll_interval);
json.put("after", after);
json.put("display", display);
json.put("hide", hide);
json.put("unified", unified);
return json;
}
@@ -233,9 +167,15 @@ public class EntityFolder implements Parcelable {
EntityFolder folder = new EntityFolder();
folder.name = json.getString("name");
folder.type = json.getString("type");
folder.unified = json.getBoolean("unified");
folder.synchronize = json.getBoolean("synchronize");
if (json.has("poll_interval"))
folder.poll_interval = json.getInt("poll_interval");
folder.after = json.getInt("after");
if (json.has("display"))
folder.display = json.getString("display");
if (json.has("hide"))
folder.hide = json.getBoolean("hide");
folder.unified = json.getBoolean("unified");
return folder;
}
}

View File

@@ -23,7 +23,6 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -253,9 +252,9 @@ public class FragmentAbout extends FragmentEx {
}
});
boolean debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
btnLog.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
btnDebugInfo.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
//boolean debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
//btnLog.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
//btnDebugInfo.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
return view;
}

View File

@@ -71,6 +71,7 @@ import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
@@ -102,6 +103,7 @@ public class FragmentAccount extends FragmentEx {
private TextInputLayout tilPassword;
private Button btnAdvanced;
private TextView tvName;
private EditText etName;
private Button btnColor;
@@ -109,11 +111,12 @@ public class FragmentAccount extends FragmentEx {
private ImageView ibColorDefault;
private EditText etSignature;
private ImageButton ibPro;
private CheckBox cbSynchronize;
private CheckBox cbPrimary;
private EditText etInterval;
private Button btnCheck;
private Button btnCheck;
private ProgressBar pbCheck;
private TextView tvIdle;
@@ -169,6 +172,7 @@ public class FragmentAccount extends FragmentEx {
tilPassword = view.findViewById(R.id.tilPassword);
btnAdvanced = view.findViewById(R.id.btnAdvanced);
etName = view.findViewById(R.id.etName);
tvName = view.findViewById(R.id.tvName);
btnColor = view.findViewById(R.id.btnColor);
@@ -576,18 +580,19 @@ public class FragmentAccount extends FragmentEx {
args.putString("password", tilPassword.getEditText().getText().toString());
args.putInt("auth_type", authorized == null ? Helper.AUTH_TYPE_PASSWORD : provider.getAuthType());
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putString("name", etName.getText().toString());
args.putInt("color", color);
args.putString("signature", Html.toHtml(etSignature.getText()));
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
args.putString("interval", etInterval.getText().toString());
args.putParcelable("drafts", drafts);
args.putParcelable("sent", sent);
args.putParcelable("all", all);
args.putParcelable("trash", trash);
args.putParcelable("junk", junk);
args.putSerializable("drafts", drafts);
args.putSerializable("sent", sent);
args.putSerializable("all", all);
args.putSerializable("trash", trash);
args.putSerializable("junk", junk);
new SimpleTask<Void>() {
@Override
@@ -599,17 +604,18 @@ public class FragmentAccount extends FragmentEx {
int auth_type = args.getInt("auth_type");
String name = args.getString("name");
int color = args.getInt("color");
Integer color = args.getInt("color");
String signature = args.getString("signature");
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
String interval = args.getString("interval");
EntityFolder drafts = args.getParcelable("drafts");
EntityFolder sent = args.getParcelable("sent");
EntityFolder all = args.getParcelable("all");
EntityFolder trash = args.getParcelable("trash");
EntityFolder junk = args.getParcelable("junk");
EntityFolder drafts = (EntityFolder) args.getSerializable("drafts");
EntityFolder sent = (EntityFolder) args.getSerializable("sent");
EntityFolder all = (EntityFolder) args.getSerializable("all");
EntityFolder trash = (EntityFolder) args.getSerializable("trash");
EntityFolder junk = (EntityFolder) args.getSerializable("junk");
if (TextUtils.isEmpty(host))
throw new Throwable(getContext().getString(R.string.title_no_host));
@@ -620,9 +626,11 @@ public class FragmentAccount extends FragmentEx {
if (TextUtils.isEmpty(password))
throw new Throwable(getContext().getString(R.string.title_no_password));
if (TextUtils.isEmpty(interval))
interval = "9";
interval = "12";
if (synchronize && drafts == null)
throw new Throwable(getContext().getString(R.string.title_no_drafts));
if (Color.TRANSPARENT == color)
color = null;
// Check IMAP server
if (synchronize) {
@@ -660,19 +668,26 @@ public class FragmentAccount extends FragmentEx {
boolean update = (account != null);
if (account == null)
account = new EntityAccount();
account.name = name;
account.color = color;
account.signature = signature;
account.host = host;
account.port = Integer.parseInt(port);
account.user = user;
account.password = password;
account.auth_type = auth_type;
account.name = name;
account.color = color;
account.signature = signature;
account.synchronize = synchronize;
account.primary = (account.synchronize && primary);
account.store_sent = false;
account.poll_interval = Integer.parseInt(interval);
if (!update)
account.seen_until = new Date().getTime();
account.store_sent = false;
if (!synchronize)
account.error = null;
@@ -1024,7 +1039,7 @@ public class FragmentAccount extends FragmentEx {
tilPassword.getEditText().setText(token);
} catch (Throwable ex) {
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
Helper.unexpectedError(getContext(), ex);
snackbar.setText(Helper.formatThrowable(ex));
} finally {
snackbar.dismiss();
}

View File

@@ -35,12 +35,14 @@ import android.os.Looper;
import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@@ -322,7 +324,16 @@ public class FragmentCompose extends FragmentEx {
public CharSequence convertToString(Cursor cursor) {
int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
return cursor.getString(colName) + " <" + cursor.getString(colEmail) + ">";
String name = cursor.getString(colName);
String email = cursor.getString(colEmail);
StringBuilder sb = new StringBuilder();
if (name == null)
sb.append(email);
else {
sb.append(name.replace(",", "")).append(" ");
sb.append("<").append(email).append(">");
}
return sb.toString();
}
});
}
@@ -379,10 +390,10 @@ public class FragmentCompose extends FragmentEx {
draftLoader.load(this, args);
}
} else {
long id = savedInstanceState.getLong("working");
working = savedInstanceState.getLong("working");
Bundle args = new Bundle();
args.putString("action", id < 0 ? "new" : "edit");
args.putLong("id", id);
args.putString("action", working < 0 ? "new" : "edit");
args.putLong("id", working);
args.putLong("account", -1);
args.putLong("reference", -1);
args.putLong("answer", -1);
@@ -413,6 +424,10 @@ public class FragmentCompose extends FragmentEx {
menu.findItem(R.id.menu_attachment).setVisible(!free && working >= 0);
menu.findItem(R.id.menu_attachment).setEnabled(etBody.isEnabled());
menu.findItem(R.id.menu_addresses).setVisible(!free && working >= 0);
PackageManager pm = getContext().getPackageManager();
menu.findItem(R.id.menu_image).setEnabled(getImageIntent().resolveActivity(pm) != null);
menu.findItem(R.id.menu_attachment).setEnabled(getAttachmentIntent().resolveActivity(pm) != null);
}
@Override
@@ -479,17 +494,11 @@ public class FragmentCompose extends FragmentEx {
}
private void onMenuImage() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, ActivityCompose.REQUEST_IMAGE);
startActivityForResult(getImageIntent(), ActivityCompose.REQUEST_IMAGE);
}
private void onMenuAttachment() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent, ActivityCompose.REQUEST_ATTACHMENT);
startActivityForResult(getAttachmentIntent(), ActivityCompose.REQUEST_ATTACHMENT);
}
private void onMenuAddresses() {
@@ -513,23 +522,18 @@ public class FragmentCompose extends FragmentEx {
}
}
private void handleExit() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
.setMessage(R.string.title_ask_delete)
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onAction(R.id.action_delete);
}
})
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
private Intent getImageIntent() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
return intent;
}
private Intent getAttachmentIntent() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
return intent;
}
private void handlePickContact(int requestCode, Intent data) {
@@ -559,9 +563,7 @@ public class FragmentCompose extends FragmentEx {
InternetAddress address = new InternetAddress(email, name);
StringBuilder sb = new StringBuilder(text);
if (sb.length() > 0)
sb.append(", ");
sb.append(address.toString());
sb.append(address.toString().replace(",", "")).append(", ");
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
etTo.setText(sb.toString());
@@ -622,6 +624,25 @@ public class FragmentCompose extends FragmentEx {
}.load(this, args);
}
private void handleExit() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
.setMessage(R.string.title_ask_delete)
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onAction(R.id.action_delete);
}
})
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
private void onAction(int action) {
Helper.setViewsEnabled(view, false);
getActivity().invalidateOptionsMenu();
@@ -636,7 +657,12 @@ public class FragmentCompose extends FragmentEx {
args.putString("cc", etCc.getText().toString());
args.putString("bcc", etBcc.getText().toString());
args.putString("subject", etSubject.getText().toString());
args.putString("body", Html.toHtml(etBody.getText()));
Spannable spannable = etBody.getText();
UnderlineSpan[] uspans = spannable.getSpans(0, spannable.length(), UnderlineSpan.class);
for (UnderlineSpan uspan : uspans)
spannable.removeSpan(uspan);
args.putString("body", Html.toHtml(spannable));
Log.i(Helper.TAG, "Run load id=" + working);
actionLoader.load(this, args);
@@ -666,7 +692,7 @@ public class FragmentCompose extends FragmentEx {
db.beginTransaction();
EntityMessage draft = db.message().getMessage(id);
Log.i(Helper.TAG, "Attaching to id=" + draft.id);
Log.i(Helper.TAG, "Attaching to id=" + id);
attachment.message = draft.id;
attachment.sequence = db.attachment().getAttachmentCount(draft.id) + 1;
@@ -1285,8 +1311,10 @@ public class FragmentCompose extends FragmentEx {
long id = Long.parseLong(cid[1].replace(BuildConfig.APPLICATION_ID + ".", ""));
File file = EntityAttachment.getFile(getContext(), id);
Drawable d = Drawable.createFromPath(file.getAbsolutePath());
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
return d;
if (d != null) {
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
return d;
}
}
}

View File

@@ -62,8 +62,6 @@ public class FragmentFolder extends FragmentEx {
private long id = -1;
private long account = -1;
private static final int MAX_FOLDER_SYNC = 25;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -320,6 +318,7 @@ public class FragmentFolder extends FragmentEx {
etDisplay.setHint(folder == null ? null : folder.name);
cbHide.setChecked(folder == null ? false : folder.hide);
cbUnified.setChecked(folder == null ? false : folder.unified);
cbSynchronize.setChecked(folder == null || folder.synchronize);
etAfter.setText(Integer.toString(folder == null ? EntityFolder.DEFAULT_USER_SYNC : folder.after));
}
@@ -327,26 +326,8 @@ public class FragmentFolder extends FragmentEx {
pbWait.setVisibility(View.GONE);
Helper.setViewsEnabled(view, true);
etRename.setEnabled(folder == null || EntityFolder.USER.equals(folder.type));
cbSynchronize.setEnabled(false);
btnSave.setEnabled(true);
ibDelete.setVisibility(folder == null || !EntityFolder.USER.equals(folder.type) ? View.GONE : View.VISIBLE);
Bundle args = new Bundle();
args.putLong("account", folder == null ? account : folder.account);
new SimpleTask<Integer>() {
@Override
protected Integer onLoad(Context context, Bundle args) {
long account = args.getLong("account");
return DB.getInstance(context).folder().getFolderSyncCount(account);
}
@Override
protected void onLoaded(Bundle args, Integer count) {
cbSynchronize.setChecked((folder == null || folder.synchronize) && count < MAX_FOLDER_SYNC);
cbSynchronize.setEnabled(count < MAX_FOLDER_SYNC);
}
}.load(FragmentFolder.this, args);
}
});
}

View File

@@ -26,6 +26,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -47,6 +48,7 @@ import android.text.method.LinkMovementMethod;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -361,6 +363,8 @@ public class FragmentMessage extends FragmentEx {
grpRawHeaders.setVisibility(View.GONE);
grpAttachments.setVisibility(View.GONE);
grpError.setVisibility(View.GONE);
setTextSize();
}
});
@@ -383,6 +387,8 @@ public class FragmentMessage extends FragmentEx {
grpRawHeaders.setVisibility(headers ? View.VISIBLE : View.GONE);
grpAttachments.setVisibility(adapter != null && adapter.getItemCount() > 0 ? View.VISIBLE : View.GONE);
setTextSize();
return true;
}
return false;
@@ -668,6 +674,7 @@ public class FragmentMessage extends FragmentEx {
boolean inOutbox = EntityFolder.OUTBOX.equals(message.folderType);
menu.findItem(R.id.menu_addresses).setVisible(!free);
menu.findItem(R.id.menu_text_size).setVisible(free);
menu.findItem(R.id.menu_thread).setVisible(message.count > 1);
menu.findItem(R.id.menu_forward).setVisible(message.content && !inOutbox);
menu.findItem(R.id.menu_show_headers).setChecked(headers);
@@ -683,6 +690,9 @@ public class FragmentMessage extends FragmentEx {
case R.id.menu_addresses:
onMenuAddresses();
return true;
case R.id.menu_text_size:
onMenuTextSize();
return true;
case R.id.menu_thread:
onMenuThread();
return true;
@@ -714,6 +724,15 @@ public class FragmentMessage extends FragmentEx {
grpAddresses.setVisibility(addresses ? View.VISIBLE : View.GONE);
}
private void onMenuTextSize() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
int size = prefs.getInt("size", 0);
size = ++size % 3;
prefs.edit().putInt("size", size).apply();
setTextSize();
}
private void onMenuThread() {
getFragmentManager().popBackStack("thread", FragmentManager.POP_BACK_STACK_INCLUSIVE);
@@ -796,26 +815,16 @@ public class FragmentMessage extends FragmentEx {
}
private void onMenuShowHtml() {
new SimpleTask<String>() {
@Override
protected String onLoad(Context context, Bundle args) throws Throwable {
return message.read(context);
}
Bundle args = new Bundle();
args.putLong("id", message.id);
args.putString("from", MessageHelper.getFormattedAddresses(message.from, true));
@Override
protected void onLoaded(Bundle a, String html) {
Bundle args = new Bundle();
args.putString("html", html);
args.putString("from", MessageHelper.getFormattedAddresses(message.from, true));
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(args);
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
fragmentTransaction.commit();
}
}.load(this, new Bundle());
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
fragmentTransaction.commit();
}
private void onMenuUnseen() {
@@ -1238,6 +1247,7 @@ public class FragmentMessage extends FragmentEx {
SpannedString ss = new SpannedString(body);
boolean has_images = (ss.getSpans(0, ss.length(), ImageSpan.class).length > 0);
setTextSize();
tvBody.setText(body);
btnImages.setVisibility(has_images && !show_images ? View.VISIBLE : View.GONE);
grpMessage.setVisibility(View.VISIBLE);
@@ -1356,4 +1366,20 @@ public class FragmentMessage extends FragmentEx {
}
});
}
private void setTextSize() {
int style = R.style.TextAppearance_AppCompat_Small;
if (free) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
int size = prefs.getInt("size", 0);
if (size == 1)
style = R.style.TextAppearance_AppCompat_Medium;
else if (size == 2)
style = R.style.TextAppearance_AppCompat_Large;
}
TypedArray ta = getContext().obtainStyledAttributes(style, new int[]{android.R.attr.textSize});
tvBody.setTextSize(TypedValue.COMPLEX_UNIT_PX, ta.getDimensionPixelSize(0, 0));
}
}

View File

@@ -182,7 +182,7 @@ public class FragmentMessages extends FragmentEx {
return 0;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
if (message.threaded || EntityFolder.OUTBOX.equals(message.folderType))
if (message == null || message.threaded || EntityFolder.OUTBOX.equals(message.folderType))
return 0;
return makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
@@ -200,6 +200,9 @@ public class FragmentMessages extends FragmentEx {
return;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
if (message == null)
return;
boolean inbox = (EntityFolder.ARCHIVE.equals(message.folderType) || EntityFolder.TRASH.equals(message.folderType));
View itemView = viewHolder.itemView;
@@ -237,6 +240,8 @@ public class FragmentMessages extends FragmentEx {
return;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
if (message == null)
return;
Log.i(Helper.TAG, "Swiped dir=" + direction + " message=" + message.id);
Bundle args = new Bundle();
@@ -265,6 +270,8 @@ public class FragmentMessages extends FragmentEx {
target = db.folder().getFolderByType(message.account, EntityFolder.ARCHIVE);
if (direction == ItemTouchHelper.LEFT || target == null)
target = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
if (target == null)
target = db.folder().getFolderByType(message.account, EntityFolder.INBOX);
}
db.message().setMessageUiHide(message.id, true);
@@ -274,7 +281,7 @@ public class FragmentMessages extends FragmentEx {
db.endTransaction();
}
Log.i(Helper.TAG, "Move id=" + id + " target=" + target);
Log.i(Helper.TAG, "Move id=" + id + " target=" + target.name);
return new String[]{target.name, target.display == null ? target.name : target.display};
}

View File

@@ -32,6 +32,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class FragmentOptions extends FragmentEx {
private CheckBox cbEnabled;
private CheckBox cbAvatars;
private CheckBox cbLight;
private CheckBox cbBrowse;
@@ -45,6 +46,7 @@ public class FragmentOptions extends FragmentEx {
View view = inflater.inflate(R.layout.fragment_options, container, false);
// Get controls
cbEnabled = view.findViewById(R.id.cbEnabled);
cbAvatars = view.findViewById(R.id.cbAvatars);
cbLight = view.findViewById(R.id.cbLight);
cbBrowse = view.findViewById(R.id.cbBrowse);
@@ -54,6 +56,15 @@ public class FragmentOptions extends FragmentEx {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
cbEnabled.setChecked(prefs.getBoolean("enabled", true));
cbEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
prefs.edit().putBoolean("enabled", checked).apply();
ServiceSynchronize.reload(getContext(), "enabled");
}
});
cbAvatars.setChecked(prefs.getBoolean("avatars", true));
cbAvatars.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override

View File

@@ -19,6 +19,7 @@ package eu.faircode.email;
Copyright 2018 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
@@ -82,11 +83,26 @@ public class FragmentWebView extends FragmentEx {
String url = args.getString("url");
webview.loadUrl(url);
setSubtitle(url);
} else if (args.containsKey("html")) {
String html = args.getString("html");
String from = args.getString("from");
webview.loadDataWithBaseURL("email://", html, "text/html", "UTF-8", null);
setSubtitle(from);
} else if (args.containsKey("id")) {
new SimpleTask<String>() {
@Override
protected String onLoad(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
return EntityMessage.read(context, id);
}
@Override
protected void onLoaded(Bundle args, String html) {
String from = args.getString("from");
webview.loadDataWithBaseURL("email://", html, "text/html", "UTF-8", null);
setSubtitle(from);
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), ex);
}
}.load(this, args);
}
((ActivityBase) getActivity()).addBackPressedListener(new ActivityBase.IBackPressedListener() {

View File

@@ -57,7 +57,6 @@ 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;
@@ -142,11 +141,6 @@ public class Helper {
}
static String formatThrowable(Throwable ex) {
if (ex instanceof FolderClosedException)
return null;
if (ex instanceof IllegalStateException)
return null;
StringBuilder sb = new StringBuilder();
sb.append(ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
Throwable cause = ex.getCause();

View File

@@ -83,7 +83,7 @@ public class MessageHelper {
// https://tools.ietf.org/html/rfc4978
// https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
if (false) {
if (true) {
Log.i(Helper.TAG, "IMAP compress enabled");
props.put("mail.imaps.compress.enable", "true");
//props.put("mail.imaps.compress.level", "-1");
@@ -91,6 +91,7 @@ public class MessageHelper {
}
props.put("mail.imaps.fetchsize", Integer.toString(48 * 1024)); // default 16K
props.put("mail.imaps.peek", "true");
// https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
props.put("mail.smtps.ssl.checkserveridentity", "true");
@@ -113,9 +114,6 @@ public class MessageHelper {
props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT));
props.put("mail.imaps.peek", "true");
//props.put("mail.imaps.minidletime", "5000");
props.put("mail.mime.address.strict", "false");
props.put("mail.mime.decodetext.strict", "false");

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Some files were not shown because too many files have changed in this diff Show More