mirror of https://github.com/M66B/FairEmail.git
Cloud sync foundation
This commit is contained in:
parent
47d0d7c6ad
commit
da6fe6f555
|
@ -168,6 +168,7 @@ android {
|
|||
buildConfigField "String", "BITBUCKET_DOWNLOADS_API", "\"https://api.bitbucket.org/2.0/repositories/M66B/fairemail-test/downloads\""
|
||||
buildConfigField "String", "BITBUCKET_DOWNLOADS_URI", "\"https://bitbucket.org/M66B/fairemail-test/downloads/\""
|
||||
buildConfigField "String", "ANNOUNCEMENT_URI", "\"https://gist.githubusercontent.com/M66B/d544192ca56224839d6ba0f2f6314c1f/raw/\""
|
||||
buildConfigField "String", "CLOUD_URI", "\"https://api.fairemail.net/sync\""
|
||||
buildConfigField "String", "TX_URI", localProperties.getProperty("paypal.uri", "\"\"")
|
||||
buildConfigField "String", "GPA_URI", localProperties.getProperty("gpa.uri", "\"\"")
|
||||
buildConfigField "String", "INFO_URI", localProperties.getProperty("info.uri", "\"\"")
|
||||
|
@ -187,6 +188,7 @@ android {
|
|||
buildConfigField "String", "BITBUCKET_DOWNLOADS_API", "\"https://api.bitbucket.org/2.0/repositories/M66B/fairemail-test/downloads\""
|
||||
buildConfigField "String", "BITBUCKET_DOWNLOADS_URI", "\"https://bitbucket.org/M66B/fairemail-test/downloads/\""
|
||||
buildConfigField "String", "ANNOUNCEMENT_URI", "\"https://gist.githubusercontent.com/M66B/d544192ca56224839d6ba0f2f6314c1f/raw/\""
|
||||
buildConfigField "String", "CLOUD_URI", "\"https://api.fairemail.net/sync\""
|
||||
buildConfigField "String", "TX_URI", "\"\""
|
||||
buildConfigField "String", "GPA_URI", "\"\""
|
||||
buildConfigField "String", "INFO_URI", "\"\""
|
||||
|
@ -207,6 +209,7 @@ android {
|
|||
buildConfigField "String", "BITBUCKET_DOWNLOADS_API", "\"\""
|
||||
buildConfigField "String", "BITBUCKET_DOWNLOADS_URI", "\"\""
|
||||
buildConfigField "String", "ANNOUNCEMENT_URI", "\"\""
|
||||
buildConfigField "String", "CLOUD_URI", "\"\""
|
||||
buildConfigField "String", "TX_URI", "\"\""
|
||||
buildConfigField "String", "GPA_URI", "\"\""
|
||||
buildConfigField "String", "INFO_URI", "\"\""
|
||||
|
@ -227,6 +230,7 @@ android {
|
|||
buildConfigField "String", "BITBUCKET_DOWNLOADS_API", "\"\""
|
||||
buildConfigField "String", "BITBUCKET_DOWNLOADS_URI", "\"\""
|
||||
buildConfigField "String", "ANNOUNCEMENT_URI", "\"\""
|
||||
buildConfigField "String", "CLOUD_URI", "\"\""
|
||||
buildConfigField "String", "TX_URI", "\"\""
|
||||
buildConfigField "String", "GPA_URI", "\"\""
|
||||
buildConfigField "String", "INFO_URI", "\"\""
|
||||
|
|
|
@ -62,6 +62,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
@ -81,12 +82,14 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.KeySpec;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -106,22 +109,45 @@ import javax.crypto.SecretKey;
|
|||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
public class FragmentOptionsBackup extends FragmentBase {
|
||||
public class FragmentOptionsBackup extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private View view;
|
||||
private ImageButton ibHelp;
|
||||
private Button btnExport;
|
||||
private Button btnImport;
|
||||
private CardView cardCloud;
|
||||
private TextView tvCloudInfo;
|
||||
private TextView tvCloudPro;
|
||||
private EditText etUser;
|
||||
private TextInputLayout tilPassword;
|
||||
private Button btnLogin;
|
||||
private TextView tvLogin;
|
||||
private CheckBox cbAccounts;
|
||||
private CheckBox cbBlockedSenders;
|
||||
private CheckBox cbFilterRules;
|
||||
private ImageButton ibSync;
|
||||
private TextView tvLastSync;
|
||||
private Button btnLogout;
|
||||
private CheckBox cbDelete;
|
||||
private Group grpLogin;
|
||||
private Group grpLogout;
|
||||
|
||||
private DateFormat DTF;
|
||||
|
||||
private static final int REQUEST_EXPORT_SELECT = 1;
|
||||
private static final int REQUEST_IMPORT_SELECT = 2;
|
||||
private static final int REQUEST_EXPORT_HANDLE = 3;
|
||||
private static final int REQUEST_IMPORT_HANDLE = 4;
|
||||
|
||||
private static final int CLOUD_TIMEOUT = 10 * 1000; // timeout
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
DTF = Helper.getDateTimeInstance(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
@ -135,9 +161,21 @@ public class FragmentOptionsBackup extends FragmentBase {
|
|||
btnExport = view.findViewById(R.id.btnExport);
|
||||
btnImport = view.findViewById(R.id.btnImport);
|
||||
cardCloud = view.findViewById(R.id.cardCloud);
|
||||
tvCloudInfo = view.findViewById(R.id.tvCloudInfo);
|
||||
tvCloudPro = view.findViewById(R.id.tvCloudPro);
|
||||
etUser = view.findViewById(R.id.etUser);
|
||||
tilPassword = view.findViewById(R.id.tilPassword);
|
||||
btnLogin = view.findViewById(R.id.btnLogin);
|
||||
tvLogin = view.findViewById(R.id.tvLogin);
|
||||
cbAccounts = view.findViewById(R.id.cbAccounts);
|
||||
cbBlockedSenders = view.findViewById(R.id.cbBlockedSenders);
|
||||
cbFilterRules = view.findViewById(R.id.cbFilterRules);
|
||||
ibSync = view.findViewById(R.id.ibSync);
|
||||
tvLastSync = view.findViewById(R.id.tvLastSync);
|
||||
btnLogout = view.findViewById(R.id.btnLogout);
|
||||
cbDelete = view.findViewById(R.id.cbDelete);
|
||||
grpLogin = view.findViewById(R.id.grpLogin);
|
||||
grpLogout = view.findViewById(R.id.grpLogout);
|
||||
|
||||
// Wire controls
|
||||
|
||||
|
@ -150,6 +188,13 @@ public class FragmentOptionsBackup extends FragmentBase {
|
|||
}
|
||||
});
|
||||
|
||||
tvCloudInfo.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Helper.viewFAQ(v.getContext(), 999);
|
||||
}
|
||||
});
|
||||
|
||||
btnExport.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -171,14 +216,86 @@ public class FragmentOptionsBackup extends FragmentBase {
|
|||
}
|
||||
});
|
||||
|
||||
cbAccounts.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
prefs.edit().putBoolean("cloud_sync_accounts", isChecked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
cbBlockedSenders.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
prefs.edit().putBoolean("cloud_sync_blocked_senders", isChecked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
cbFilterRules.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
prefs.edit().putBoolean("cloud_sync_filter_rules", isChecked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
ibSync.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// TODO
|
||||
}
|
||||
});
|
||||
|
||||
btnLogout.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onLogout();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
FragmentDialogTheme.setBackground(getContext(), view, false);
|
||||
cardCloud.setVisibility(BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
? View.VISIBLE : View.GONE);
|
||||
cardCloud.setVisibility(
|
||||
BuildConfig.DEBUG &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
!TextUtils.isEmpty(BuildConfig.CLOUD_URI)
|
||||
? View.VISIBLE : View.GONE);
|
||||
Helper.linkPro(tvCloudPro);
|
||||
|
||||
prefs.registerOnSharedPreferenceChangeListener(this);
|
||||
cbAccounts.setChecked(prefs.getBoolean("cloud_sync_accounts", true));
|
||||
cbBlockedSenders.setChecked(prefs.getBoolean("cloud_sync_blocked_senders", true));
|
||||
cbFilterRules.setChecked(prefs.getBoolean("cloud_sync_filter_rules", true));
|
||||
onSharedPreferenceChanged(prefs, null);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
||||
if (key == null ||
|
||||
"cloud_user".equals(key) ||
|
||||
"cloud_password".equals(key)) {
|
||||
String user = prefs.getString("cloud_user", null);
|
||||
String password = prefs.getString("cloud_password", null);
|
||||
boolean auth = !(TextUtils.isEmpty(user) || TextUtils.isEmpty(password));
|
||||
long last_sync = prefs.getLong("cloud_last_sync", 0);
|
||||
|
||||
etUser.setText(user);
|
||||
tilPassword.getEditText().setText(password);
|
||||
tvLogin.setText(user);
|
||||
tvLastSync.setText(getString(R.string.title_advanced_cloud_last_sync,
|
||||
last_sync == 0 ? "-" : DTF.format(last_sync)));
|
||||
cbDelete.setChecked(false);
|
||||
grpLogin.setVisibility(auth ? View.GONE : View.VISIBLE);
|
||||
grpLogout.setVisibility(auth ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
@ -1362,46 +1479,142 @@ public class FragmentOptionsBackup extends FragmentBase {
|
|||
}
|
||||
|
||||
private void onLogin() {
|
||||
String username = etUser.getText().toString();
|
||||
String password = tilPassword.getEditText().getText().toString();
|
||||
|
||||
if (TextUtils.isEmpty(username.trim())) {
|
||||
etUser.requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
tilPassword.getEditText().requestFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString("user", etUser.getText().toString());
|
||||
cloud(args);
|
||||
}
|
||||
|
||||
private void onLogout() {
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean("logout", true);
|
||||
args.putBoolean("wipe", cbDelete.isChecked());
|
||||
cloud(args);
|
||||
}
|
||||
|
||||
private void cloud(Bundle args) {
|
||||
args.putString("user", etUser.getText().toString().trim());
|
||||
args.putString("password", tilPassword.getEditText().getText().toString());
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
new SimpleTask<String>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
btnLogin.setEnabled(false);
|
||||
Helper.setViewsEnabled(cardCloud, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bundle args) {
|
||||
btnLogin.setEnabled(true);
|
||||
Helper.setViewsEnabled(cardCloud, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void onExecute(Context context, Bundle args) throws Throwable {
|
||||
protected String onExecute(Context context, Bundle args) throws Throwable {
|
||||
String user = args.getString("user");
|
||||
String password = args.getString("password");
|
||||
boolean wipe = args.getBoolean("wipe");
|
||||
|
||||
Pair<byte[], byte[]> key = getKeyPair(user, password);
|
||||
byte[] salt = MessageDigest.getInstance("SHA256").digest(user.getBytes());
|
||||
String cloudUser = Helper.hex(MessageDigest.getInstance("SHA256").digest(salt));
|
||||
|
||||
return null;
|
||||
Pair<byte[], byte[]> key = getKeyPair(salt, password);
|
||||
String cloudPassword = Helper.hex(key.first);
|
||||
|
||||
JSONObject jroot = new JSONObject();
|
||||
jroot.put("username", cloudUser);
|
||||
jroot.put("password", cloudPassword);
|
||||
jroot.put("wipe", wipe);
|
||||
jroot.put("debug", BuildConfig.DEBUG);
|
||||
String request = jroot.toString();
|
||||
Log.i("Cloud request=" + request);
|
||||
|
||||
URL url = new URL(BuildConfig.CLOUD_URI);
|
||||
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoInput(true);
|
||||
connection.setDoOutput(true);
|
||||
connection.setReadTimeout(CLOUD_TIMEOUT);
|
||||
connection.setConnectTimeout(CLOUD_TIMEOUT);
|
||||
ConnectionHelper.setUserAgent(context, connection);
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.connect();
|
||||
|
||||
try {
|
||||
connection.getOutputStream().write(request.getBytes());
|
||||
|
||||
int status = connection.getResponseCode();
|
||||
if (status != HttpsURLConnection.HTTP_OK) {
|
||||
String error = "Error " + status + ": " + connection.getResponseMessage();
|
||||
String detail = Helper.readStream(connection.getErrorStream());
|
||||
JSONObject jerror = new JSONObject(detail);
|
||||
if (status == HttpsURLConnection.HTTP_FORBIDDEN)
|
||||
throw new SecurityException(jerror.optString("error"));
|
||||
else
|
||||
throw new IOException(error + " " + jerror);
|
||||
}
|
||||
|
||||
String response = Helper.readStream(connection.getInputStream());
|
||||
Log.i("Cloud response=" + response);
|
||||
JSONObject jresponse = new JSONObject(response);
|
||||
return jresponse.optString("status");
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExecuted(Bundle args, Void data) {
|
||||
protected void onExecuted(Bundle args, String status) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
if ("ok".equals(status) && !args.getBoolean("logout"))
|
||||
prefs.edit()
|
||||
.putString("cloud_user", args.getString("user"))
|
||||
.putString("cloud_password", args.getString("password"))
|
||||
.apply();
|
||||
else
|
||||
prefs.edit()
|
||||
.remove("cloud_user")
|
||||
.remove("cloud_password")
|
||||
.apply();
|
||||
|
||||
view.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
view.scrollTo(0, cardCloud.getTop());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
if (ex instanceof SecurityException) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
|
||||
.setIcon(R.drawable.twotone_warning_24)
|
||||
.setTitle(getString(R.string.title_advanced_cloud_invalid))
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
String message = ex.getMessage();
|
||||
if (!TextUtils.isEmpty(message))
|
||||
builder.setMessage(message);
|
||||
builder.show();
|
||||
} else
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
}
|
||||
}.execute(FragmentOptionsBackup.this, args, "cloud:login");
|
||||
}.execute(FragmentOptionsBackup.this, args, "cloud");
|
||||
}
|
||||
|
||||
private static Pair<byte[], byte[]> getKeyPair(String user, String password)
|
||||
private static Pair<byte[], byte[]> getKeyPair(byte[] salt, String password)
|
||||
throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
byte[] salt = MessageDigest.getInstance("SHA256").digest(user.getBytes());
|
||||
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
|
||||
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||
KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, 310000, 2 * 256);
|
||||
|
|
|
@ -2242,7 +2242,11 @@ public class Log {
|
|||
Object value = settings.get(key);
|
||||
if ("wipe_mnemonic".equals(key) && value != null)
|
||||
value = "[redacted]";
|
||||
if (key != null && key.startsWith("oauth."))
|
||||
else if ("cloud_user".equals(key) && value != null)
|
||||
value = "[redacted]";
|
||||
else if ("cloud_password".equals(key) && value != null)
|
||||
value = "[redacted]";
|
||||
else if (key != null && key.startsWith("oauth."))
|
||||
value = "[redacted]";
|
||||
size += write(os, key + "=" + value + "\r\n");
|
||||
}
|
||||
|
|
|
@ -168,6 +168,32 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCloudInfo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:drawableEnd="@drawable/twotone_info_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/title_advanced_cloud_security"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloud" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCloudPro"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_pro_feature"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="?android:attr/textColorLink"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudInfo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvUser"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -177,7 +203,7 @@
|
|||
android:text="@string/title_user"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloud" />
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCloudPro" />
|
||||
|
||||
<eu.faircode.email.EditTextPlain
|
||||
android:id="@+id/etUser"
|
||||
|
@ -223,12 +249,122 @@
|
|||
style="?android:attr/buttonStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:drawableEnd="@drawable/twotone_login_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/title_advanced_login"
|
||||
android:tag="disable"
|
||||
android:text="@string/title_advanced_cloud_login"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvRegister"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_register"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnLogin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogin"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="username"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvRegister" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbAccounts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_accounts"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvLogin" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbBlockedSenders"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_blocked_senders"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbAccounts" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbFilterRules"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_cloud_filter_rules"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbBlockedSenders" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/ibSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cbFilterRules"
|
||||
app:srcCompat="@drawable/twotone_compare_arrows_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLastSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_advanced_cloud_last_sync"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ibSync" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnLogout"
|
||||
style="?android:attr/buttonStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:drawableEnd="@drawable/twotone_logout_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:tag="disable"
|
||||
android:text="@string/title_advanced_cloud_logout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvLastSync" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:drawableEnd="@drawable/twotone_warning_24"
|
||||
android:drawablePadding="6dp"
|
||||
android:tag="disable"
|
||||
android:text="@string/title_advanced_cloud_wipe"
|
||||
app:drawableTint="?attr/colorWarning"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnLogout" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/grpLogin"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="
|
||||
tvUser,etUser,tvPassword,tilPassword,btnLogin,tvRegister" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/grpLogout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="
|
||||
tvLogin,cbAccounts,cbBlockedSenders,cbFilterRules,
|
||||
ibSync,tvLastSync,
|
||||
btnLogout,cbDelete" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -972,7 +972,17 @@
|
|||
<string name="title_advanced_cleanup_hint">This will delete all temporary files</string>
|
||||
<string name="title_advanced_keywords_hint" translatable="false">Space separated</string>
|
||||
|
||||
<string name="title_advanced_login" translatable="false">Login</string>
|
||||
<string name="title_advanced_cloud_security" translatable="false">All data is encrypted end-to-end and the cloud server will never see the username, password and data</string>
|
||||
<string name="title_advanced_cloud_login" translatable="false">Login</string>
|
||||
<string name="title_advanced_cloud_register" translatable="false">Logging in for the first time will automatically create an account</string>
|
||||
<string name="title_advanced_cloud_invalid" translatable="false">Invalid username or password</string>
|
||||
<string name="title_advanced_cloud_accounts" translatable="false">Sync accounts</string>
|
||||
<string name="title_advanced_cloud_blocked_senders" translatable="false">Sync blocked senders</string>
|
||||
<string name="title_advanced_cloud_filter_rules" translatable="false">Sync filter rules</string>
|
||||
<string name="title_advanced_cloud_last_sync" translatable="false">Last sync: %1$s</string>
|
||||
<string name="title_advanced_cloud_update" translatable="false">Update settings</string>
|
||||
<string name="title_advanced_cloud_logout" translatable="false">Logout</string>
|
||||
<string name="title_advanced_cloud_wipe" translatable="false">Wipe cloud data on logout</string>
|
||||
|
||||
<string name="title_advanced_never_favorite">Never favorite</string>
|
||||
<string name="title_advanced_edit_name">Edit name</string>
|
||||
|
|
Loading…
Reference in New Issue