/*
* Copyright (c) 2015 OpenSilk Productions LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package syncthing.android.ui.sessionsettings;
import android.content.Context;
import android.databinding.Bindable;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.app.AlertDialog;
import android.view.MenuItem;
import android.view.View;
import android.widget.PopupMenu;
import org.apache.commons.lang3.StringUtils;
import org.opensilk.common.core.dagger2.ForApplication;
import org.opensilk.common.core.dagger2.ScreenScope;
import org.opensilk.common.ui.mortar.ActivityResultsController;
import org.opensilk.common.ui.mortar.DialogPresenter;
import org.opensilk.common.ui.mortar.ToolbarOwner;
import javax.inject.Inject;
import rx.functions.Action1;
import syncthing.android.R;
import syncthing.android.service.SyncthingUtils;
import syncthing.android.settings.AppSettings;
import syncthing.api.Credentials;
import syncthing.api.SessionManager;
import syncthing.api.model.Config;
import syncthing.api.model.DeviceConfig;
import syncthing.api.model.GUIConfig;
import syncthing.api.model.OptionsConfig;
import timber.log.Timber;
/**
* Created by drew on 3/17/15.
*/
@ScreenScope
public class SettingsPresenter extends EditPresenter<CoordinatorLayout> {
final Context appContext;
final AppSettings appSettings;
DeviceConfig thisDevice;
OptionsConfig options;
GUIConfig guiConfig;
String hiddenPass;
String errorListenAddress;
String errorGlobalDiscoverServers;
String errorGuiListenAddress;
@Inject
public SettingsPresenter(
SessionManager manager,
DialogPresenter dialogPresenter,
ActivityResultsController activityResultContoller,
ToolbarOwner toolbarOwner,
EditPresenterConfig config,
@ForApplication Context appContext,
AppSettings appSettings
) {
super(manager, dialogPresenter, activityResultContoller, toolbarOwner, config);
this.appContext = appContext;
this.appSettings = appSettings;
}
@Override
protected void onLoad(Bundle savedInstanceState) {
super.onLoad(savedInstanceState);
if (!wasPreviouslyLoaded && savedInstanceState != null) {
thisDevice = (DeviceConfig) savedInstanceState.getSerializable("device");
options = (OptionsConfig) savedInstanceState.getSerializable("options");
guiConfig = (GUIConfig) savedInstanceState.getSerializable("guiconfig");
} else if (!wasPreviouslyLoaded) {
DeviceConfig d = controller.getThisDevice();
if (d != null) {
thisDevice = d.clone();
}
Config config = controller.getConfig();
if (config != null) {
OptionsConfig o = config.options;
if (o != null) {
options = o.clone();
}
GUIConfig g = config.gui;
if (g != null) {
guiConfig = g.clone();
}
}
}
wasPreviouslyLoaded = true;
if (thisDevice == null || options == null || guiConfig == null) {
Timber.e("Incomplete data! Cannot continue");
dismissDialog();
}
hiddenPass = getView().getContext().getString(R.string.ten_stars);
}
@Override
protected void onSave(Bundle outState) {
super.onSave(outState);
//TODO parcelable
outState.putSerializable("device", thisDevice);
outState.putSerializable("options", options);
outState.putSerializable("guiconfig", guiConfig);
}
@Bindable
public String getDeviceName() {
return SyncthingUtils.getDisplayName(thisDevice);
}
public void setDeviceName(CharSequence deviceName) {
thisDevice.name = StringUtils.isEmpty(deviceName) ? "" : deviceName.toString();
}
public final Action1<CharSequence> actionSetDeviceName = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setDeviceName(charSequence);
}
};
@Bindable
public String getListenAddressText() {
return SyncthingUtils.unrollArray(options.listenAddress);
}
public void setListenAddress(CharSequence text) {
if (validateListenAddresses(text)) {
options.listenAddress = SyncthingUtils.rollArray(text.toString());
}
}
public final Action1<CharSequence> actionSetListenAddress = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setListenAddress(charSequence);
}
};
@Bindable
public String getListenAddressError() {
return errorListenAddress;
}
public void setListenAddressError(String error) {
if (!StringUtils.equals(errorListenAddress, error)) {
errorListenAddress = error;
notifyChange(syncthing.android.BR.listenAddressError);
}
}
boolean validateListenAddresses(CharSequence text) {
int err = 0;
if (StringUtils.isEmpty(text)) {
err = R.string.input_error;
} else {
String string = StringUtils.trim(text.toString());
if (!StringUtils.startsWith(string, "tcp://")) {
err = R.string.input_error;
} //todo ip w/ port or ?
}
if (hasView()) {
String msg = err != 0 ? getView().getContext().getString(err) : null;
setGuiListenAddressError(msg);
}
return err == 0;
}
@Bindable
public String getMaxRecvKbps() {
return String.valueOf(options.maxRecvKbps);
}
public void setMaxRecvKpbs(CharSequence text) {
//input disallows non numeric text
options.maxRecvKbps = StringUtils.isEmpty(text) ? 0 : Integer.valueOf(text.toString());
}
public final Action1<CharSequence> actionSetMaxRecvKbps = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setMaxRecvKpbs(charSequence);
}
};
@Bindable
public String getMaxSendKbps() {
return String.valueOf(options.maxSendKbps);
}
public void setMaxSendKbps(CharSequence text) {
//input disallows non numeric text
options.maxSendKbps = StringUtils.isEmpty(text) ? 0 : Integer.valueOf(text.toString());
}
public final Action1<CharSequence> actionSetMaxSendKbps = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setMaxSendKbps(charSequence);
}
};
@Bindable
public boolean isUpnpEnabled() {
return options.upnpEnabled;
}
public void setUpnpEnabled(boolean enabled) {
options.upnpEnabled = enabled;
}
public final Action1<Boolean> actionSetUpnpEnabled = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setUpnpEnabled(aBoolean);
}
};
@Bindable
public boolean isLocalAnnounceEnabled() {
return options.localAnnounceEnabled;
}
public void setLocalAnnounceEnabled(boolean enabled) {
options.localAnnounceEnabled = enabled;
}
public final Action1<Boolean> actionSetLocalAnnounceEnabled = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setLocalAnnounceEnabled(aBoolean);
}
};
@Bindable
public boolean isGlobalAnnounceEnabled() {
return options.globalAnnounceEnabled;
}
public void setGlobalAnnounceEnabled(boolean enabled) {
options.globalAnnounceEnabled = enabled;
notifyChange(syncthing.android.BR.globalAnnounceEnabled);
}
public final Action1<Boolean> actionSetGlobalAnnounceEnabled = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setGlobalAnnounceEnabled(aBoolean);
}
};
@Bindable
public String getGlobalAnnounceServersText() {
return SyncthingUtils.unrollArray(options.globalAnnounceServers);
}
public void setGlobalAnnounceServers(CharSequence text) {
if (validateGlobalDiscoveryServers(text)) {
options.globalAnnounceServers = SyncthingUtils.rollArray(text.toString());
}
}
public final Action1<CharSequence> actionSetGlobalAnnounceServers = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setGlobalAnnounceServers(charSequence);
}
};
@Bindable
public String getGlobalAnnounceServersError() {
return errorGlobalDiscoverServers;
}
public void setGlobalAnnounceServersError(String text) {
if (!StringUtils.equals(errorGlobalDiscoverServers, text)) {
errorGlobalDiscoverServers = text;
notifyChange(syncthing.android.BR.globalAnnounceServersError);
}
}
boolean validateGlobalDiscoveryServers(CharSequence text) {
int err = 0;
if (StringUtils.isEmpty(text)) {
err = R.string.input_error;
} else {
//TODO dynamic dynamic-v4 dynamic-v6 or hosts or ips
// (too many combinations so just use user discretion)
}
if (hasView()) {
String msg = err != 0 ? getView().getContext().getString(err) : null;
setGuiListenAddressError(msg);
}
return err == 0;
}
@Bindable
public String getGuiListenAddress() {
return guiConfig.address;
}
public void setGuiListenAddress(CharSequence text) {
if (validateGuiListenAddress(text)) {
guiConfig.address = text.toString();
}
}
public final Action1<CharSequence> actionSetGuiListenAddress = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setGuiListenAddress(charSequence);
}
};
@Bindable
public String getGuiListenAddressError() {
return errorGuiListenAddress;
}
public void setGuiListenAddressError(String text) {
if (!StringUtils.equals(errorGuiListenAddress, text)) {
errorGuiListenAddress = text;
notifyChange(syncthing.android.BR.guiListenAddressError);
}
}
boolean validateGuiListenAddress(CharSequence text) {
int err = 0;
if (StringUtils.isEmpty(text)) {
err = R.string.input_error;
} else {
String string = StringUtils.trim(text.toString());
if (!SyncthingUtils.isIpAddressWithPort(string)) {
err = R.string.input_error;
}
}
if (hasView()) {
String msg = err != 0 ? getView().getContext().getString(err) : null;
setGuiListenAddressError(msg);
}
return err == 0;
}
@Bindable
public String getGuiUser() {
return guiConfig.user;
}
public void setGuiUser(CharSequence text) {
guiConfig.user = StringUtils.isEmpty(text) ? "" : text.toString();
}
public final Action1<CharSequence> actionSetGuiUser = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setGuiUser(charSequence);
}
};
public void setGuiPassword(CharSequence text) {
if (StringUtils.isEmpty(text)) {
guiConfig.password = "";
} else if (!StringUtils.equals(hiddenPass, text.toString())) {
guiConfig.password = text.toString();
}
}
public final Action1<CharSequence> actionSetGuiPassword = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setGuiPassword(charSequence);
}
};
@Bindable
public boolean isUseTLS() {
return guiConfig.useTLS;
}
public void setUseTLS(boolean enable) {
guiConfig.useTLS = enable;
}
public final Action1<Boolean> actionSetUseTLS = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setUseTLS(aBoolean);
}
};
@Bindable
public boolean isStartBrowser() {
return options.startBrowser;
}
public void setStartBrowser(boolean enable) {
options.startBrowser = enable;
}
public final Action1<Boolean> actionSetStartBrowser = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setStartBrowser(aBoolean);
}
};
@Bindable
public boolean isURAccepted() {
return options.urAccepted > 0;
}
public void setURAccepted(boolean enable) {
options.urAccepted = enable ? 1 : -1;
}
public final Action1<Boolean> actionSetURAccepted = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setURAccepted(aBoolean);
}
};
@Bindable
public String getApiKey() {
return guiConfig.apiKey;
}
public void setApiKey(String text) {
guiConfig.apiKey = text;
}
@Bindable
public boolean isHasClipboard() {
return !SyncthingUtils.isClipBoardSupported(appContext);
}
public void showApiKeyOverflow(final View btn) {
PopupMenu m = new PopupMenu(btn.getContext(), btn);
m.inflate(R.menu.apikey_overflow);
m.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.copy:
copyApiKey(btn);
return true;
case R.id.generate:
regenApiKey(btn);
return true;
default:
return false;
}
}
});
m.show();
}
public void copyApiKey(View btn) {
SyncthingUtils.copyToClipboard(btn.getContext(),
btn.getContext().getString(R.string.api_key),
getApiKey());
}
public void regenApiKey(View btn) {
String key = SyncthingUtils.randomString(32);
setApiKey(key);
notifyChange(syncthing.android.BR.apiKey);
}
public void saveConfig(View btn) {
boolean invalid = false;
invalid |= errorListenAddress != null;
invalid |= errorGlobalDiscoverServers != null;
invalid |= errorGuiListenAddress != null;
if (invalid) {
dialogPresenter.showDialog(context -> new AlertDialog.Builder(context)
.setTitle(R.string.input_error)
.setMessage(R.string.input_error_message)
.setPositiveButton(android.R.string.cancel, null)
.setNegativeButton(R.string.save, (d,w) -> {
saveConfig();
})
.create());
} else {
saveConfig();
}
}
private void saveConfig() {
unsubscribe(saveSubscription);
onSaveStart();
final String deviceName = thisDevice.name;
saveSubscription = controller.editSettings(thisDevice, options, guiConfig,
this::onSavefailed,
() -> {
maybeUpdateAlias(deviceName);
onSaveSuccessfull();
}
);
}
void maybeUpdateAlias(String deviceName) {
if (!StringUtils.isEmpty(deviceName)
&& !StringUtils.equalsIgnoreCase(credentials.alias, deviceName)) {
appSettings.saveCredentials(new Credentials(deviceName, credentials.id,
credentials.url, credentials.apiKey, credentials.caCert));
}
}
}