Compare commits

...

20 Commits
1.342 ... 1.345

Author SHA1 Message Date
M66B
8773ab2959 1.345 release 2019-02-28 16:53:17 +00:00
M66B
fa66ac9246 Added compose action button to unified inbox folders 2019-02-28 16:48:18 +00:00
M66B
26290a693a Use identity IP by default 2019-02-28 16:05:55 +00:00
M66B
c8238ae1ae Round duration to next hour 2019-02-28 15:53:29 +00:00
M66B
9e224f5918 Switch to date/time spinners 2019-02-28 15:50:37 +00:00
M66B
712e1a079c Update account state on on demand sync 2019-02-28 15:25:10 +00:00
M66B
e6cfe55aa2 Refactoring 2019-02-28 15:19:15 +00:00
M66B
561b2e699d 1.344 release 2019-02-28 12:55:24 +00:00
M66B
4b8a4b92bc Use on demand sync when disabled 2019-02-28 12:35:19 +00:00
M66B
1324a364e8 Small state fix 2019-02-28 11:28:25 +00:00
M66B
525380404a 1.343 release 2019-02-28 11:10:18 +00:00
M66B
064fafeca2 Crowdin sync 2019-02-28 11:08:11 +00:00
M66B
9880f10a5b Fixed folder import compatibility 2019-02-28 11:04:34 +00:00
M66B
693f6d91ae Properly reset on demand state 2019-02-28 11:03:14 +00:00
M66B
6692aa0179 Updated format throwable 2019-02-28 10:46:59 +00:00
M66B
9391a0f2a3 Added folders refresh 2019-02-28 10:43:46 +00:00
M66B
59b13d15dd Updated FAQ 2019-02-28 10:12:22 +00:00
M66B
f667d17a83 Prevent crash 2019-02-28 10:03:03 +00:00
M66B
b4742a2072 Revert "Sync folders on manual sync"
This reverts commit 03cd7f927e.
2019-02-28 09:39:04 +00:00
M66B
f6d810fab1 Fixed starting send service on boot 2019-02-28 09:37:37 +00:00
25 changed files with 1907 additions and 164 deletions

1
FAQ.md
View File

@@ -30,6 +30,7 @@ For authorizing:
* ~~Synchronize on demand (manual)~~
* ~~Semi-automatic encryption~~
* Add message copy
Anything on this list is in random order and *might* be added in the near future.

View File

@@ -10,8 +10,8 @@ android {
applicationId "eu.faircode.email"
minSdkVersion 21
targetSdkVersion 28
versionCode 342
versionName "1.342"
versionCode 345
versionName "1.345"
archivesBaseName = "FairEmail-v$versionName"
javaCompileOptions {

File diff suppressed because it is too large Load Diff

View File

@@ -135,7 +135,8 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
vwLevel.setLayoutParams(lp);
}
if (folder.sync_state == null || "requested".equals(folder.sync_state)) {
if (folder.sync_state == null ||
"requested".equals(folder.sync_state) || "manual".equals(folder.sync_state)) {
if ("waiting".equals(folder.state))
ivState.setImageResource(R.drawable.baseline_hourglass_empty_24);
else if ("connected".equals(folder.state))
@@ -145,16 +146,11 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
else if ("closing".equals(folder.state))
ivState.setImageResource(R.drawable.baseline_close_24);
else if (folder.state == null)
if ("requested".equals(folder.sync_state))
ivState.setImageResource(R.drawable.baseline_hourglass_empty_24);
else
ivState.setImageResource(R.drawable.baseline_cloud_off_24);
ivState.setImageResource(R.drawable.baseline_cloud_off_24);
else
ivState.setImageResource(android.R.drawable.stat_sys_warning);
} else {
if ("requested".equals(folder.sync_state))
ivState.setImageResource(R.drawable.baseline_hourglass_empty_24);
else if ("syncing".equals(folder.sync_state))
if ("syncing".equals(folder.sync_state))
ivState.setImageResource(R.drawable.baseline_compare_arrows_24);
else if ("downloading".equals(folder.sync_state))
ivState.setImageResource(R.drawable.baseline_cloud_download_24);
@@ -298,6 +294,9 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
NetworkInfo ni = cm.getActiveNetworkInfo();
boolean internet = (ni != null && ni.isConnected());
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean enabled = prefs.getBoolean("enabled", true);
DB db = DB.getInstance(context);
try {
db.beginTransaction();
@@ -312,7 +311,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
} else {
EntityAccount account = db.account().getAccount(aid);
if (account.ondemand) {
if (account.ondemand || !enabled) {
if (internet) {
now = true;
ServiceUI.sync(context, fid);
@@ -320,7 +319,7 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
} else {
now = "connected".equals(account.state);
EntityOperation.sync(context, db, fid, true);
EntityOperation.sync(context, db, fid);
}
}

View File

@@ -185,8 +185,6 @@ class Core {
break;
case EntityOperation.SYNC:
if (jargs.getBoolean(3))
onSynchronizeFolders(context, account, istore, state);
onSynchronizeMessages(context, jargs, account, folder, (IMAPFolder) ifolder, state);
break;

View File

@@ -49,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 47,
version = 48,
entities = {
EntityIdentity.class,
EntityAccount.class,
@@ -523,6 +523,13 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `identity` ADD COLUMN `use_ip` INTEGER NOT NULL DEFAULT 0");
}
})
.addMigrations(new Migration(47, 48) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("UPDATE `identity` SET use_ip = 1");
}
})
.build();
}

View File

@@ -83,7 +83,7 @@ public interface DaoAccount {
", (SELECT COUNT(operation.id) FROM operation" +
" JOIN folder ON folder.id = operation.folder" +
" JOIN account ON account.id = folder.account" + // not outbox
" WHERE account.synchronize) AS operations")
" WHERE account.synchronize) AS operations") // including on demand
LiveData<TupleAccountStats> liveStats();
@Query("SELECT account.id, swipe_left, l.type AS left_type, swipe_right, r.type AS right_type" +

View File

@@ -41,7 +41,15 @@ public interface DaoFolder {
@Query("SELECT folder.* FROM folder" +
" JOIN account ON account.id = folder.account" +
" WHERE account.synchronize AND folder.synchronize AND unified")
" WHERE folder.synchronize" +
" AND account.id = :account" +
" AND (account.synchronize AND account.ondemand)")
List<EntityFolder> getFoldersOnDemandSync(long account);
@Query("SELECT folder.* FROM folder" +
" JOIN account ON account.id = folder.account" +
" WHERE account.synchronize" +
" AND folder.synchronize AND unified")
List<EntityFolder> getFoldersSynchronizingUnified();
@Query("SELECT folder.* FROM folder" +
@@ -74,7 +82,7 @@ public interface DaoFolder {
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
" FROM folder" +
" JOIN account ON account.id = folder.account" +
" JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" LEFT JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" WHERE account.`synchronize`" +
" AND folder.unified" +
" GROUP BY folder.id")

View File

@@ -29,7 +29,7 @@ import androidx.room.Query;
@Dao
public interface DaoOperation {
@Query("SELECT operation.*, account.name AS accountName, folder.name AS folderName" +
" ,((account.synchronize IS NULL OR account.synchronize)" +
" ,((account.synchronize IS NULL OR account.synchronize)" + // including on demand
" AND (NOT folder.account IS NULL OR identity.synchronize IS NULL OR identity.synchronize)) AS synchronize" +
" FROM operation" +
" JOIN folder ON folder.id = operation.folder" +
@@ -50,7 +50,7 @@ public interface DaoOperation {
" LEFT JOIN account ON account.id = message.account" +
" LEFT JOIN identity ON identity.id = message.identity" +
" WHERE operation.folder = :folder" +
" AND (account.synchronize IS NULL OR account.synchronize)" +
" AND (account.synchronize IS NULL OR account.synchronize)" + // including on demand
" AND (NOT folder.account IS NULL OR identity.synchronize IS NULL OR identity.synchronize)" +
" ORDER BY" +
" CASE WHEN operation.name = '" + EntityOperation.SYNC + "' THEN" +

View File

@@ -3,12 +3,14 @@ package eu.faircode.email;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.DatePicker;
import android.widget.TextView;
import android.widget.TimePicker;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
@@ -17,14 +19,18 @@ import androidx.lifecycle.LifecycleOwner;
public class DialogDuration {
static void show(Context context, LifecycleOwner owner, int title, final IDialogDuration intf) {
final View dview = LayoutInflater.from(context).inflate(R.layout.dialog_duration, null);
final TextView tvDuration = dview.findViewById(R.id.tvDuration);
final TimePicker timePicker = dview.findViewById(R.id.timePicker);
final DatePicker datePicker = dview.findViewById(R.id.datePicker);
final Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(new Date().getTime() / (60 * 1000L) * (60 * 1000L));
cal.setTimeInMillis((new Date().getTime() / (3600 * 1000L) + 1) * (3600 * 1000L));
Log.i("Set init=" + new Date(cal.getTimeInMillis()));
timePicker.setIs24HourView(DateFormat.is24HourFormat(context));
final DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.FULL, SimpleDateFormat.SHORT);
tvDuration.setText(df.format(cal.getTime()));
timePicker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(context));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
timePicker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
timePicker.setCurrentMinute(cal.get(Calendar.MINUTE));
@@ -38,6 +44,7 @@ public class DialogDuration {
public void onTimeChanged(TimePicker view, int hour, int minute) {
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
tvDuration.setText(df.format(cal.getTime()));
Log.i("Set hour=" + hour + " minute=" + minute +
" time=" + new Date(cal.getTimeInMillis()));
}
@@ -53,6 +60,7 @@ public class DialogDuration {
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.DAY_OF_MONTH, day);
tvDuration.setText(df.format(cal.getTime()));
Log.i("Set year=" + year + " month=" + month + " day=" + day +
" time=" + new Date(cal.getTimeInMillis()));
}

View File

@@ -290,7 +290,9 @@ public class EntityFolder implements Serializable {
public static EntityFolder fromJSON(JSONObject json) throws JSONException {
EntityFolder folder = new EntityFolder();
folder.id = json.getLong("id");
if (json.has("id"))
folder.id = json.getLong("id");
folder.name = json.getString("name");
folder.type = json.getString("type");

View File

@@ -72,7 +72,7 @@ public class EntityIdentity {
public String password;
public String realm;
@NonNull
public Boolean use_ip = false; // instead of domain name
public Boolean use_ip = true; // instead of domain name
@NonNull
public Boolean synchronize;
@NonNull

View File

@@ -99,22 +99,15 @@ public class EntityOperation {
}
static void sync(Context context, DB db, long fid) {
sync(context, db, fid, false);
}
static void sync(Context context, DB db, long fid, boolean folders) {
if (db.operation().getOperationCount(fid, EntityOperation.SYNC) == 0) {
EntityFolder folder = db.folder().getFolder(fid);
JSONArray jargs = folder.getSyncArgs();
jargs.put(folders);
EntityOperation operation = new EntityOperation();
operation.folder = folder.id;
operation.message = null;
operation.name = SYNC;
operation.args = jargs.toString();
operation.args = folder.getSyncArgs().toString();
operation.created = new Date().getTime();
operation.id = db.operation().insertOperation(operation);

View File

@@ -19,7 +19,12 @@ package eu.faircode.email;
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
@@ -31,6 +36,7 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.util.List;
@@ -39,10 +45,14 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Observer;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
public class FragmentFolders extends FragmentBase {
private ViewGroup view;
private SwipeRefreshLayout swipeRefresh;
private ImageButton ibHintActions;
private ImageButton ibHintSync;
private RecyclerView rvFolder;
@@ -71,9 +81,10 @@ public class FragmentFolders extends FragmentBase {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
View view = inflater.inflate(R.layout.fragment_folders, container, false);
view = (ViewGroup) inflater.inflate(R.layout.fragment_folders, container, false);
// Get controls
swipeRefresh = view.findViewById(R.id.swipeRefresh);
ibHintActions = view.findViewById(R.id.ibHintActions);
ibHintSync = view.findViewById(R.id.ibHintSync);
rvFolder = view.findViewById(R.id.rvFolder);
@@ -87,6 +98,16 @@ public class FragmentFolders extends FragmentBase {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
int colorPrimary = Helper.resolveColor(getContext(), R.attr.colorPrimary);
swipeRefresh.setColorSchemeColors(Color.WHITE, Color.WHITE, Color.WHITE);
swipeRefresh.setProgressBackgroundColorSchemeColor(colorPrimary);
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
onSwipeRefresh();
}
});
ibHintActions.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -113,16 +134,52 @@ public class FragmentFolders extends FragmentBase {
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Bundle args = new Bundle();
args.putLong("account", account);
FragmentFolder fragment = new FragmentFolder();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folder");
fragmentTransaction.commit();
if (account < 0) {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("action", "new")
.putExtra("account", account)
);
} else {
Bundle args = new Bundle();
args.putLong("account", account);
FragmentFolder fragment = new FragmentFolder();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("folder");
fragmentTransaction.commit();
}
}
});
if (account < 0)
fab.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
new SimpleTask<EntityFolder>() {
@Override
protected EntityFolder onExecute(Context context, Bundle args) {
return DB.getInstance(context).folder().getPrimaryDrafts();
}
@Override
protected void onExecuted(Bundle args, EntityFolder drafts) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
.putExtra("account", drafts.account)
.putExtra("folder", drafts.id));
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentFolders.this, new Bundle(), "folders:drafts");
return true;
}
});
// Initialize
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
@@ -154,9 +211,22 @@ public class FragmentFolders extends FragmentBase {
DB db = DB.getInstance(getContext());
// Observe account
if (account < 0)
if (account < 0) {
setSubtitle(R.string.title_folders_unified);
else
fab.setImageResource(R.drawable.baseline_edit_24);
db.identity().liveComposableIdentities(null).observe(getViewLifecycleOwner(),
new Observer<List<TupleIdentityEx>>() {
@Override
public void onChanged(List<TupleIdentityEx> identities) {
if (identities == null || identities.size() == 0)
fab.hide();
else
fab.show();
}
});
} else
db.account().liveAccount(account).observe(getViewLifecycleOwner(), new Observer<EntityAccount>() {
@Override
public void onChanged(@Nullable EntityAccount account) {
@@ -204,6 +274,96 @@ public class FragmentFolders extends FragmentBase {
});
}
private void onSwipeRefresh() {
Bundle args = new Bundle();
args.putLong("account", account);
new SimpleTask<Boolean>() {
@Override
protected void onPostExecute(Bundle args) {
swipeRefresh.setRefreshing(false);
}
@Override
protected Boolean onExecute(Context context, Bundle args) {
long aid = args.getLong("account");
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = cm.getActiveNetworkInfo();
boolean internet = (ni != null && ni.isConnected());
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean enabled = prefs.getBoolean("enabled", true);
DB db = DB.getInstance(context);
try {
db.beginTransaction();
boolean now = false;
boolean nointernet = false;
if (aid < 0) {
// Unified inbox
List<EntityFolder> folders = db.folder().getFoldersSynchronizingUnified();
for (EntityFolder folder : folders) {
EntityAccount account = db.account().getAccount(folder.account);
if (account.ondemand || !enabled)
if (internet) {
now = true;
ServiceUI.sync(context, folder.id);
} else
nointernet = true;
else {
now = "connected".equals(account.state);
EntityOperation.sync(context, db, folder.id);
}
}
} else {
EntityAccount account = db.account().getAccount(aid);
if (account.ondemand || !enabled) {
if (internet) {
now = true;
List<EntityFolder> folders = db.folder().getFoldersOnDemandSync(aid);
for (EntityFolder folder : folders)
ServiceUI.sync(context, folder.id);
} else
nointernet = true;
} else {
if (internet) {
now = true;
ServiceSynchronize.reload(getContext(), "refresh folders");
} else
nointernet = true;
}
}
db.setTransactionSuccessful();
if (nointernet)
throw new IllegalArgumentException(context.getString(R.string.title_no_internet));
return now;
} finally {
db.endTransaction();
}
}
@Override
protected void onExecuted(Bundle args, Boolean now) {
if (!now)
Snackbar.make(view, R.string.title_sync_delayed, Snackbar.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentFolders.this, args, "folders:refresh");
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_folders, menu);

View File

@@ -757,7 +757,7 @@ public class FragmentIdentity extends FragmentBase {
etUser.setText(identity == null ? null : identity.user);
tilPassword.getEditText().setText(identity == null ? null : identity.password);
etRealm.setText(identity == null ? null : identity.realm);
cbUseIp.setChecked(identity == null ? false : identity.use_ip);
cbUseIp.setChecked(identity == null ? true : identity.use_ip);
cbSynchronize.setChecked(identity == null ? true : identity.synchronize);
cbPrimary.setChecked(identity == null ? true : identity.primary);

View File

@@ -496,6 +496,9 @@ public class FragmentMessages extends FragmentBase {
NetworkInfo ni = cm.getActiveNetworkInfo();
boolean internet = (ni != null && ni.isConnected());
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean enabled = prefs.getBoolean("enabled", true);
DB db = DB.getInstance(context);
try {
db.beginTransaction();
@@ -523,7 +526,7 @@ public class FragmentMessages extends FragmentBase {
nointernet = true;
} else {
EntityAccount account = db.account().getAccount(folder.account);
if (account.ondemand) {
if (account.ondemand || !enabled) {
if (internet) {
now = true;
ServiceUI.sync(context, folder.id);
@@ -531,7 +534,7 @@ public class FragmentMessages extends FragmentBase {
nointernet = true;
} else {
now = "connected".equals(account.state);
EntityOperation.sync(context, db, folder.id, true);
EntityOperation.sync(context, db, folder.id);
}
}
@@ -557,7 +560,6 @@ public class FragmentMessages extends FragmentBase {
@Override
protected void onException(Bundle args, Throwable ex) {
swipeRefresh.setRefreshing(false);
if (ex instanceof IllegalArgumentException)
Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show();
else

View File

@@ -21,8 +21,6 @@ package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -59,6 +57,7 @@ import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.sun.mail.util.FolderClosedIOException;
import com.sun.mail.util.MailConnectException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -74,6 +73,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
@@ -97,7 +97,6 @@ import javax.net.ssl.SSLException;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -312,6 +311,8 @@ public class Helper {
return null;
if (ex instanceof SSLException || ex.getCause() instanceof SSLException)
return null;
if (ex instanceof MailConnectException && ex.getCause() instanceof UnknownHostException)
return null;
}
StringBuilder sb = new StringBuilder();

View File

@@ -54,6 +54,7 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleService;
import androidx.lifecycle.Observer;
@@ -452,6 +453,7 @@ public class ServiceSend extends LifecycleService {
}
static void start(Context context) {
context.startService(new Intent(context, ServiceSend.class));
ContextCompat.startForegroundService(context,
new Intent(context, ServiceSend.class));
}
}

View File

@@ -11,6 +11,7 @@ import android.preference.PreferenceManager;
import com.sun.mail.imap.IMAPFolder;
import java.util.Date;
import java.util.Properties;
import javax.mail.Folder;
@@ -242,14 +243,15 @@ public class ServiceUI extends IntentService {
Log.i("Synchronize on demand folder=" + fid);
DB db = DB.getInstance(this);
EntityFolder folder = null;
EntityAccount account = null;
EntityFolder folder = db.folder().getFolder(fid);
if (folder == null)
return;
EntityAccount account = db.account().getAccount(folder.account);
if (account == null)
return;
Store istore = null;
try {
folder = db.folder().getFolder(fid);
account = db.account().getAccount(folder.account);
// Create session
Properties props = MessageHelper.getSessionProperties(account.auth_type, account.realm, account.insecure);
final Session isession = Session.getInstance(props, null);
@@ -261,6 +263,8 @@ public class ServiceUI extends IntentService {
istore = isession.getStore(account.getProtocol());
Helper.connect(this, istore, account);
db.account().setAccountState(account.id, "connected");
db.account().setAccountConnected(account.id, new Date().getTime());
db.account().setAccountError(account.id, null);
Log.i(account.name + " connected");
// Synchronize folders
@@ -298,16 +302,19 @@ public class ServiceUI extends IntentService {
Log.e(ex);
}
db.account().setAccountState(account.id, null);
db.folder().setFolderState(folder.id, null);
Log.i(account.name + " closed");
}
db.account().setAccountState(account.id, null);
db.folder().setFolderState(folder.id, null);
db.folder().setFolderSyncState(folder.id, null);
}
}
public static void sync(Context context, long folder) {
DB db = DB.getInstance(context);
db.folder().setFolderSyncState(folder, "requested");
db.folder().setFolderState(folder, "waiting");
db.folder().setFolderSyncState(folder, "manual");
context.startService(
new Intent(context, ServiceUI.class)

View File

@@ -33,7 +33,8 @@ public class TupleFolderEx extends EntityFolder {
boolean isSynchronizing() {
return (sync_state != null &&
(EntityFolder.OUTBOX.equals(type) ||
accountOnDemand || "connected".equals(accountState)));
!"requested".equals(sync_state) ||
"connected".equals(accountState)));
}
@Override

View File

@@ -9,18 +9,32 @@
android:layout_height="wrap_content"
android:padding="24dp">
<TextView
android:id="@+id/tvDuration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Thurday 28 February 2019 06:45 am"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TimePicker
android:id="@+id/timePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:timePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/tvDuration" />
<DatePicker
android:id="@+id/datePicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timePicker" />

View File

@@ -6,119 +6,124 @@
android:layout_height="match_parent"
tools:context=".ActivityView">
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ActivityView">
android:layout_height="match_parent">
<TextView
android:id="@+id/tvHintActions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:gravity="center_vertical"
android:minHeight="33dp"
android:text="@string/title_hint_folder_actions"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toStartOf="@+id/ibHintActions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/ibHintActions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/tvHintActions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvHintActions" />
<View
android:id="@+id/vSeparatorActions"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHintActions" />
android:layout_height="match_parent">
<TextView
android:id="@+id/tvHintSync"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:gravity="center_vertical"
android:minHeight="33dp"
android:text="@string/title_hint_folder_sync"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toStartOf="@+id/ibHintSync"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorActions" />
<TextView
android:id="@+id/tvHintActions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:gravity="center_vertical"
android:minHeight="33dp"
android:text="@string/title_hint_folder_actions"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toStartOf="@+id/ibHintActions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/ibHintSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/tvHintSync"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvHintSync" />
<ImageButton
android:id="@+id/ibHintActions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/tvHintActions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvHintActions" />
<View
android:id="@+id/vSeparatorSync"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHintSync" />
<View
android:id="@+id/vSeparatorActions"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHintActions" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFolder"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorSync" />
<TextView
android:id="@+id/tvHintSync"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:gravity="center_vertical"
android:minHeight="33dp"
android:text="@string/title_hint_folder_sync"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toStartOf="@+id/ibHintSync"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorActions" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/ibHintSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/tvHintSync"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tvHintSync" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpHintActions"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvHintActions,ibHintActions,vSeparatorActions" />
<View
android:id="@+id/vSeparatorSync"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="6dp"
android:background="?attr/colorSeparator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHintSync" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpHintSync"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvHintSync,ibHintSync,vSeparatorSync" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFolder"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/vSeparatorSync" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="rvFolder" />
</androidx.constraintlayout.widget.ConstraintLayout>
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpHintActions"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvHintActions,ibHintActions,vSeparatorActions" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpHintSync"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvHintSync,ibHintSync,vSeparatorSync" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpReady"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="rvFolder" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@@ -213,7 +213,6 @@
android:layout_height="0dp"
app:constraint_referenced_ids="rvMessage" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton

View File

@@ -44,6 +44,7 @@
<item quantity="other">¿Reportar %1$d mensajes como spam?</item>
</plurals>
<string name="title_ask_spam_who">¿Reportar mensaje de %1$s como spam?</string>
<string name="title_notification_sending">Enviando mensajes</string>
<string name="title_notification_failed">\'%1$s\' falló</string>
<string name="menu_answers">Plantillas</string>
<string name="menu_operations">Operaciones</string>

View File

@@ -36,6 +36,7 @@
<item quantity="other">报告 %1$d 为垃圾邮件?</item>
</plurals>
<string name="title_ask_spam_who">将来自%1$s的消息举报为垃圾邮件?</string>
<string name="title_notification_sending">正发送消息</string>
<string name="title_notification_failed">“%1$s”失败</string>
<string name="menu_answers">模板</string>
<string name="menu_operations">操作</string>