mirror of
https://github.com/M66B/FairEmail.git
synced 2026-01-22 15:47:58 +01:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8773ab2959 | ||
|
|
fa66ac9246 | ||
|
|
26290a693a | ||
|
|
c8238ae1ae | ||
|
|
9e224f5918 | ||
|
|
712e1a079c | ||
|
|
e6cfe55aa2 | ||
|
|
561b2e699d | ||
|
|
4b8a4b92bc | ||
|
|
1324a364e8 | ||
|
|
525380404a | ||
|
|
064fafeca2 | ||
|
|
9880f10a5b | ||
|
|
693f6d91ae | ||
|
|
6692aa0179 | ||
|
|
9391a0f2a3 | ||
|
|
59b13d15dd | ||
|
|
f667d17a83 | ||
|
|
b4742a2072 | ||
|
|
f6d810fab1 |
1
FAQ.md
1
FAQ.md
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1534
app/schemas/eu.faircode.email.DB/48.json
Normal file
1534
app/schemas/eu.faircode.email.DB/48.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user