mirror of https://github.com/tasks/tasks
Add EteSyncAccountSettingsActivity
parent
f97df9e884
commit
744a673c7b
@ -0,0 +1,29 @@
|
||||
package org.tasks.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.core.util.Pair;
|
||||
import com.etesync.journalmanager.Crypto.AsymmetricKeyPair;
|
||||
import org.tasks.etesync.EteSyncClient;
|
||||
import org.tasks.gtasks.PlayServices;
|
||||
import org.tasks.ui.CompletableViewModel;
|
||||
|
||||
public class AddEteSyncAccountViewModel
|
||||
extends CompletableViewModel<Pair<String, String>> {
|
||||
public void addAccount(
|
||||
PlayServices playServices,
|
||||
Context context,
|
||||
EteSyncClient client,
|
||||
String url,
|
||||
String username,
|
||||
String password,
|
||||
String encryptionPassword) {
|
||||
run(
|
||||
() -> {
|
||||
playServices.updateSecurityProvider(context);
|
||||
return client
|
||||
.setForeground()
|
||||
.forUrl(url, username, encryptionPassword, null)
|
||||
.getKeyAndToken(password);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.tasks.activities;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.core.util.Pair;
|
||||
import org.tasks.etesync.EteSyncClient;
|
||||
import org.tasks.ui.CompletableViewModel;
|
||||
|
||||
public class UpdateEteSyncAccountViewModel extends CompletableViewModel<Pair<String, String>> {
|
||||
public void addAccount(
|
||||
Context context,
|
||||
EteSyncClient client,
|
||||
String url,
|
||||
String username,
|
||||
String password,
|
||||
String encryptionPassword) {
|
||||
run(
|
||||
() ->
|
||||
client
|
||||
.setForeground()
|
||||
.forUrl(url, username, encryptionPassword, null)
|
||||
.getKeyAndToken(password));
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package org.tasks.etesync;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import butterknife.OnFocusChange;
|
||||
import butterknife.OnTextChanged;
|
||||
import com.todoroo.astrid.helper.UUIDHelper;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.R;
|
||||
import org.tasks.activities.AddEteSyncAccountViewModel;
|
||||
import org.tasks.activities.UpdateEteSyncAccountViewModel;
|
||||
import org.tasks.analytics.Tracking.Events;
|
||||
import org.tasks.caldav.BaseCaldavAccountSettingsActivity;
|
||||
import org.tasks.data.CaldavAccount;
|
||||
import org.tasks.gtasks.PlayServices;
|
||||
import org.tasks.injection.ActivityComponent;
|
||||
import org.tasks.injection.ForApplication;
|
||||
|
||||
public class EteSyncAccountSettingsActivity extends BaseCaldavAccountSettingsActivity
|
||||
implements Toolbar.OnMenuItemClickListener {
|
||||
|
||||
@Inject @ForApplication Context context;
|
||||
@Inject PlayServices playServices;
|
||||
@Inject EteSyncClient eteSyncClient;
|
||||
|
||||
private AddEteSyncAccountViewModel addAccountViewModel;
|
||||
private UpdateEteSyncAccountViewModel updateAccountViewModel;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding.repeat.setVisibility(View.GONE);
|
||||
binding.encryptionPasswordLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
addAccountViewModel = ViewModelProviders.of(this).get(AddEteSyncAccountViewModel.class);
|
||||
updateAccountViewModel = ViewModelProviders.of(this).get(UpdateEteSyncAccountViewModel.class);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
if (caldavAccount == null) {
|
||||
binding.url.setText(R.string.etesync_url);
|
||||
}
|
||||
}
|
||||
|
||||
addAccountViewModel.observe(this, this::addAccount, this::requestFailed);
|
||||
updateAccountViewModel.observe(this, this::updateAccount, this::requestFailed);
|
||||
}
|
||||
|
||||
private void addAccount(Pair<String, String> authentication) {
|
||||
CaldavAccount newAccount = new CaldavAccount();
|
||||
newAccount.setAccountType(CaldavAccount.TYPE_ETESYNC);
|
||||
newAccount.setUuid(UUIDHelper.newUUID());
|
||||
applyTo(newAccount, authentication);
|
||||
newAccount.setId(caldavDao.insert(newAccount));
|
||||
|
||||
tracker.reportEvent(Events.CALDAV_ACCOUNT_ADDED);
|
||||
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void updateAccount(Pair<String, String> authentication) {
|
||||
applyTo(caldavAccount, authentication);
|
||||
caldavAccount.setError("");
|
||||
caldavDao.update(caldavAccount);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void applyTo(CaldavAccount account, @Nullable Pair<String, String> authentication) {
|
||||
account.setName(getNewName());
|
||||
account.setUrl(getNewURL());
|
||||
account.setUsername(getNewUsername());
|
||||
if (authentication != null) {
|
||||
account.setPassword(encryption.encrypt(authentication.first));
|
||||
account.setEncryptionKey(encryption.encrypt(authentication.second));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needsValidation() {
|
||||
return super.needsValidation() || encryptionPasswordChanged();
|
||||
}
|
||||
|
||||
protected boolean encryptionPasswordChanged() {
|
||||
return caldavAccount == null
|
||||
|| !PASSWORD_MASK.equals(binding.encryptionPassword.getText().toString().trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addAccount(String url, String username, String password) {
|
||||
addAccountViewModel.addAccount(
|
||||
playServices, context, eteSyncClient, url, username, password, getNewEncryptionPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateAccount(String url, String username, String password) {
|
||||
updateAccountViewModel.addAccount(
|
||||
context, eteSyncClient, url, username, password, getNewEncryptionPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateAccount() {
|
||||
updateAccount(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@OnTextChanged(R.id.encryption_password)
|
||||
void onEncryptionPasswordChanged(CharSequence text) {
|
||||
binding.encryptionPasswordLayout.setError(null);
|
||||
}
|
||||
|
||||
@OnFocusChange(R.id.encryption_password)
|
||||
void onEncryptionPasswordFocused(boolean hasFocus) {
|
||||
changePasswordFocus(binding.encryptionPassword, hasFocus);
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package org.tasks.etesync;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
import at.bitfire.cert4android.CustomCertManager;
|
||||
import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier;
|
||||
import com.etesync.journalmanager.Crypto;
|
||||
import com.etesync.journalmanager.Crypto.CryptoManager;
|
||||
import com.etesync.journalmanager.Exceptions;
|
||||
import com.etesync.journalmanager.Exceptions.IntegrityException;
|
||||
import com.etesync.journalmanager.Exceptions.VersionTooNewException;
|
||||
import com.etesync.journalmanager.JournalAuthenticator;
|
||||
import com.etesync.journalmanager.JournalManager;
|
||||
import com.etesync.journalmanager.JournalManager.Journal;
|
||||
import com.etesync.journalmanager.UserInfoManager;
|
||||
import com.etesync.journalmanager.UserInfoManager.UserInfo;
|
||||
import com.etesync.journalmanager.model.CollectionInfo;
|
||||
import com.etesync.journalmanager.util.TokenAuthenticator;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.OkHttpClient.Builder;
|
||||
import okhttp3.internal.tls.OkHostnameVerifier;
|
||||
import org.tasks.DebugNetworkInterceptor;
|
||||
import org.tasks.caldav.MemoryCookieStore;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.preferences.Preferences;
|
||||
import org.tasks.security.Encryption;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class EteSyncClient {
|
||||
|
||||
private final Encryption encryption;
|
||||
private final Preferences preferences;
|
||||
private final DebugNetworkInterceptor interceptor;
|
||||
private final String username;
|
||||
private final String encryptionPassword;
|
||||
private final OkHttpClient httpClient;
|
||||
private final HttpUrl httpUrl;
|
||||
private final Context context;
|
||||
private final JournalManager journalManager;
|
||||
private boolean foreground;
|
||||
|
||||
@Inject
|
||||
public EteSyncClient(
|
||||
@ForApplication Context context,
|
||||
Encryption encryption,
|
||||
Preferences preferences,
|
||||
DebugNetworkInterceptor interceptor) {
|
||||
this.context = context;
|
||||
this.encryption = encryption;
|
||||
this.preferences = preferences;
|
||||
this.interceptor = interceptor;
|
||||
username = null;
|
||||
encryptionPassword = null;
|
||||
httpClient = null;
|
||||
httpUrl = null;
|
||||
journalManager = null;
|
||||
}
|
||||
|
||||
private EteSyncClient(
|
||||
Context context,
|
||||
Encryption encryption,
|
||||
Preferences preferences,
|
||||
DebugNetworkInterceptor interceptor,
|
||||
String url,
|
||||
String username,
|
||||
String encryptionPassword,
|
||||
String token,
|
||||
boolean foreground)
|
||||
throws NoSuchAlgorithmException, KeyManagementException {
|
||||
this.context = context;
|
||||
this.encryption = encryption;
|
||||
this.preferences = preferences;
|
||||
this.interceptor = interceptor;
|
||||
this.username = username;
|
||||
this.encryptionPassword = encryptionPassword;
|
||||
|
||||
CustomCertManager customCertManager = new CustomCertManager(context);
|
||||
customCertManager.setAppInForeground(foreground);
|
||||
CustomHostnameVerifier hostnameVerifier =
|
||||
customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[] {customCertManager}, null);
|
||||
|
||||
Builder builder =
|
||||
new OkHttpClient()
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(new TokenAuthenticator(null, token))
|
||||
.cookieJar(new MemoryCookieStore())
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(true)
|
||||
.sslSocketFactory(sslContext.getSocketFactory(), customCertManager)
|
||||
.hostnameVerifier(hostnameVerifier)
|
||||
.readTimeout(30, TimeUnit.SECONDS);
|
||||
if (preferences.isFlipperEnabled()) {
|
||||
interceptor.add(builder);
|
||||
}
|
||||
httpClient = builder.build();
|
||||
httpUrl = HttpUrl.parse(url);
|
||||
journalManager = new JournalManager(httpClient, httpUrl);
|
||||
}
|
||||
|
||||
public EteSyncClient forUrl(String url, String username, String encryptionPassword, String token)
|
||||
throws KeyManagementException, NoSuchAlgorithmException {
|
||||
return new EteSyncClient(
|
||||
context,
|
||||
encryption,
|
||||
preferences,
|
||||
interceptor,
|
||||
url,
|
||||
username,
|
||||
encryptionPassword,
|
||||
token,
|
||||
foreground);
|
||||
}
|
||||
|
||||
public Pair<String, String> getKeyAndToken(String password)
|
||||
throws IOException, Exceptions.HttpException, VersionTooNewException, IntegrityException {
|
||||
JournalAuthenticator journalAuthenticator = new JournalAuthenticator(httpClient, httpUrl);
|
||||
String token = journalAuthenticator.getAuthToken(username, password);
|
||||
UserInfoManager userInfoManager = new UserInfoManager(httpClient, httpUrl);
|
||||
UserInfo userInfo = userInfoManager.fetch(username);
|
||||
String key = Crypto.deriveKey(username, encryptionPassword);
|
||||
CryptoManager cryptoManager = new CryptoManager(userInfo.getVersion(), key, "userInfo");
|
||||
userInfo.verify(cryptoManager);
|
||||
return Pair.create(token, key);
|
||||
}
|
||||
|
||||
public CryptoManager getCrypto(Journal journal)
|
||||
throws VersionTooNewException, IntegrityException {
|
||||
return new CryptoManager(journal.getVersion(), encryptionPassword, journal.getUid());
|
||||
}
|
||||
|
||||
private @Nullable CollectionInfo convertJournalToCollection(Journal journal) {
|
||||
try {
|
||||
CryptoManager cryptoManager = getCrypto(journal);
|
||||
journal.verify(cryptoManager);
|
||||
CollectionInfo collection =
|
||||
CollectionInfo.Companion.fromJson(journal.getContent(cryptoManager));
|
||||
collection.updateFromJournal(journal);
|
||||
return collection;
|
||||
} catch (IntegrityException | VersionTooNewException e) {
|
||||
Timber.e(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Journal, CollectionInfo> getCalendars() throws Exceptions.HttpException {
|
||||
Map<Journal, CollectionInfo> result = new HashMap<>();
|
||||
for (Journal journal : journalManager.list()) {
|
||||
CollectionInfo collection = convertJournalToCollection(journal);
|
||||
if (collection != null && collection.getType().equals("TASKS")) {
|
||||
result.put(journal, collection);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public EteSyncClient setForeground() {
|
||||
foreground = true;
|
||||
return this;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue