Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6927650e78 | ||
|
|
0246aab0bf | ||
|
|
4eb1988427 | ||
|
|
2cc3c07651 | ||
|
|
f351bccb8e | ||
|
|
ce57552886 | ||
|
|
350fbdf486 | ||
|
|
b84a4ded3c | ||
|
|
38171d08ad | ||
|
|
d9476a291a | ||
|
|
781201ad64 | ||
|
|
5a356ab23c | ||
|
|
f731535e3c | ||
|
|
7810193a80 | ||
|
|
4c3d158d82 | ||
|
|
f26b915ece | ||
|
|
2fa0d298cf | ||
|
|
74349ed1cf | ||
|
|
6b3821c82c | ||
|
|
926ac6cf0a | ||
|
|
43d7add8f4 | ||
|
|
828ba46bed | ||
|
|
2f702d6a64 | ||
|
|
ebeccf71d8 | ||
|
|
1071688459 | ||
|
|
6789ad9c3c | ||
|
|
e602b27fa3 | ||
|
|
5466e06b07 | ||
|
|
362f025267 | ||
|
|
97678efb6b | ||
|
|
a3341db947 | ||
|
|
4228a86431 | ||
|
|
643770b139 | ||
|
|
67b2f1ca5b | ||
|
|
7f35a89413 | ||
|
|
cbc4bfc22e | ||
|
|
195acc4bf2 | ||
|
|
35d996bc4d | ||
|
|
e4cb13b9ec | ||
|
|
9ecdfaafa4 | ||
|
|
7a55f8b8ed | ||
|
|
681a8a31e2 | ||
|
|
0d340d1865 | ||
|
|
155506ce81 | ||
|
|
bf310662c1 | ||
|
|
2f0babc479 | ||
|
|
b19f496ce0 | ||
|
|
1140ae3bf8 | ||
|
|
b75088833e | ||
|
|
8f784c8954 | ||
|
|
70fbc5aa6a | ||
|
|
095a885d30 | ||
|
|
38df2f6d24 | ||
|
|
3a53d69b31 | ||
|
|
215fbbb74e | ||
|
|
b867e93291 | ||
|
|
e496278128 | ||
|
|
e7292f0fc9 | ||
|
|
f4a25a359d | ||
|
|
ee4339a971 | ||
|
|
0bf88fcf9e | ||
|
|
11d355c3d5 | ||
|
|
ed881a42ad | ||
|
|
1fe8784b1f | ||
|
|
5fe713180d | ||
|
|
c0fa6acc33 | ||
|
|
09b530dd9d | ||
|
|
3f0aa1b59a | ||
|
|
a4949c6473 | ||
|
|
64493bb573 | ||
|
|
6bb0d05478 | ||
|
|
d3fc9aadcc | ||
|
|
d7c4064f0b | ||
|
|
7a47f40fe1 | ||
|
|
a2ab19206c | ||
|
|
faf36edef0 | ||
|
|
34884e7e85 | ||
|
|
aa138cabc0 | ||
|
|
87817b9412 | ||
|
|
11e9333f14 | ||
|
|
703d8af6e8 | ||
|
|
0c2b035970 | ||
|
|
ad218d7b49 | ||
|
|
a0fc3c4058 | ||
|
|
695bb5bc8a | ||
|
|
6786358082 | ||
|
|
f93e4ca813 | ||
|
|
4fb2e7dbc2 | ||
|
|
84eeadd2f7 | ||
|
|
e41cc0cccf | ||
|
|
4d5d202ed7 |
BIN
.idea/caches/build_file_checksums.ser
generated
47
FAQ.md
@@ -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, there’s 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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
1000
app/schemas/eu.faircode.email.DB/20.json
Normal 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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 185 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 236 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 262 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 147 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 200 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 325 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 305 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 325 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 322 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 398 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 585 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 484 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 484 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 421 B |