/*
* 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.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.databinding.Bindable;
import android.databinding.BindingAdapter;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import com.jakewharton.rxbinding.widget.RxCompoundButton;
import org.apache.commons.lang3.StringUtils;
import org.opensilk.common.core.dagger2.ScreenScope;
import org.opensilk.common.ui.mortar.ActivityResultsController;
import org.opensilk.common.ui.mortar.ActivityResultsListener;
import org.opensilk.common.ui.mortar.DialogPresenter;
import org.opensilk.common.ui.mortar.ToolbarOwner;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.inject.Inject;
import mortar.MortarScope;
import rx.Subscription;
import rx.functions.Action1;
import syncthing.android.R;
import syncthing.android.service.SyncthingUtils;
import syncthing.android.ui.common.ActivityRequestCodes;
import syncthing.api.SessionManager;
import syncthing.api.model.Compression;
import syncthing.api.model.DeviceConfig;
import syncthing.api.model.FolderConfig;
import syncthing.api.model.FolderDeviceConfig;
import timber.log.Timber;
/**
* Created by drew on 3/16/15.
*/
@ScreenScope
public class EditDevicePresenter extends EditPresenter<CoordinatorLayout> implements ActivityResultsListener {
DeviceConfig originalDevice;
TreeMap<String, Boolean> sharedFolders;
Subscription deleteSubscription;
String errorDeviceId;
String errorAddresses;
@Inject
public EditDevicePresenter(
SessionManager manager,
DialogPresenter dialogPresenter,
ActivityResultsController activityResultContoller,
ToolbarOwner toolbarOwner,
EditPresenterConfig config
) {
super(manager, dialogPresenter, activityResultContoller, toolbarOwner, config);
}
@Override
protected void onEnterScope(MortarScope scope) {
super.onEnterScope(scope);
activityResultsController.register(scope, this);
}
@Override
protected void onExitScope() {
super.onExitScope();
unsubscribe(deleteSubscription);
}
@Override @SuppressWarnings("unchecked")
protected void onLoad(Bundle savedInstanceState) {
super.onLoad(savedInstanceState);
if (!wasPreviouslyLoaded && savedInstanceState != null) {
originalDevice = (DeviceConfig) savedInstanceState.getSerializable("device");
sharedFolders = (TreeMap<String, Boolean>) savedInstanceState.getSerializable("folders");
} else if (!wasPreviouslyLoaded) {
if (isAdd) {
originalDevice = DeviceConfig.withDefaults();
} else {
DeviceConfig d = controller.getDevice(deviceId);
if (d != null) {
originalDevice = d.clone();
}
}
sharedFolders = new TreeMap<>();
List<FolderConfig> folders = controller.getFolders();
for (FolderConfig f : folders) {
sharedFolders.put(f.id, false);
if (!StringUtils.isEmpty(getDeviceID())) {
for (FolderDeviceConfig d : f.devices) {
if (StringUtils.equals(d.deviceID, getDeviceID())) {
sharedFolders.put(f.id, true);
break;
}
}
}
}
}
wasPreviouslyLoaded = true;
if (originalDevice == null) {
Timber.e("Null device! Cannot continue");
dismissDialog();
}
}
@Override
protected void onSave(Bundle outState) {
super.onSave(outState);
outState.putSerializable("device", originalDevice);
outState.putSerializable("folders", sharedFolders);
}
@Bindable
public boolean isAdd() {
return isAdd;
}
@Bindable
public String getDeviceID() {
return originalDevice.deviceID;
}
public void setDeviceID(CharSequence deviceID) {
if (validateDeviceId(deviceID, true)) {
originalDevice.deviceID = deviceID.toString();
}
}
public final Action1<CharSequence> actionSetDeviceID = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setDeviceID(charSequence);
}
};
@Bindable
public String getDeviceIDError() {
return errorDeviceId;
}
public void setDeviceIDError(String error) {
if (!StringUtils.equals(errorDeviceId, error)) {
errorDeviceId = error;
notifyChange(syncthing.android.BR.deviceIDError);
}
}
boolean validateDeviceId(CharSequence text, boolean strict) {
int e = 0;
if (StringUtils.isEmpty(text)) {
e = R.string.the_device_id_cannot_be_blank;
} else if (strict && !StringUtils.remove(text.toString(), ' ').matches(("^[- \\w\\s]{50,64}$"))) {
e = R.string.the_entered_device_id_does_not_look_valid_it_should_be_a_52_or_56_character_string_consisting_of_letters_and_numbers_with_spaces_and_dashes_being_optional;
}
if (hasView()) {
setDeviceIDError(e != 0 ? getView().getContext().getString(e) : null);
}
return e == 0;
}
@Bindable
public String getDeviceName() {
return originalDevice.name;
}
public void setDeviceName(CharSequence name) {
originalDevice.name = StringUtils.isEmpty(name) ? "" : name.toString();
}
public final Action1<CharSequence> actionSetDeviceName = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setDeviceName(charSequence);
}
};
@Bindable
public String getAddressesText() {
return SyncthingUtils.unrollArray(originalDevice.addresses);
}
public void setAddresses(CharSequence addressesText) {
if (validateAddresses(addressesText)) {
originalDevice.addresses = SyncthingUtils.rollArray(addressesText.toString());
}
}
public final Action1<CharSequence> actionSetAddresses = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setAddresses(charSequence);
}
};
@Bindable
public String getAddressesError() {
return errorAddresses;
}
public void setAddressesError(String text) {
if (!StringUtils.equals(errorAddresses, text)) {
errorAddresses = text;
notifyChange(syncthing.android.BR.addressesError);
}
}
boolean validateAddresses(CharSequence text) {
boolean invalid = false;
if (StringUtils.isEmpty(text.toString())) {
invalid = true;
} else {
String s = text.toString().trim();
if (!StringUtils.contains(s, ',')) {
if (!StringUtils.equals(s, "dynamic") && !validateAddress(s)) {
invalid = true;
}
} else {
String[] addrs = SyncthingUtils.rollArray(s);
for (String addr : addrs) {
if (!validateAddress(addr.trim())) {
invalid = true;
break;
}
}
}
}
if (hasView()) {
setAddressesError(invalid ? getView().getContext().getString(R.string.input_error) : null);
}
return !invalid;
}
boolean validateAddress(String addr) {
return StringUtils.startsWith(addr.trim(), "tcp://") &&
(SyncthingUtils.isIpAddressWithPort(StringUtils.removeStart(addr.trim(), "tcp://")) ||
SyncthingUtils.isDomainNameWithPort(StringUtils.removeStart(addr.trim(), "tcp://")));
}
@Bindable
public Compression getCompression() {
return originalDevice.compression;
}
public void setCompression(Compression compression) {
originalDevice.compression = compression;
}
public final Action1<Integer> actionOnCompressionChanged = new Action1<Integer>() {
@Override
public void call(Integer checkedId) {
switch (checkedId) {
case R.id.radio_all_compression:
setCompression(Compression.ALWAYS);
break;
case R.id.radio_meta_compression:
setCompression(Compression.METADATA);
break;
case R.id.radio_no_compression:
setCompression(Compression.NEVER);
break;
}
}
};
@Bindable
public boolean isIntroducer() {
return originalDevice.introducer;
}
public void setIntroducer(boolean introducer) {
originalDevice.introducer = introducer;
}
public final Action1<Boolean> actionSetIntroducer = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setIntroducer(aBoolean);
}
};
public void setFolderShared(String folderId, boolean shared) {
sharedFolders.put(folderId, shared);
}
@BindingAdapter("addShareFolders")
public static void addSharedFolders(LinearLayout shareFoldersContainer, EditDevicePresenter presenter) {
if (presenter == null) return;
shareFoldersContainer.removeAllViews();
for (Map.Entry<String, Boolean> e : presenter.sharedFolders.entrySet()) {
final String id = e.getKey();
CheckBox checkBox = new CheckBox(shareFoldersContainer.getContext());
checkBox.setText(id);
checkBox.setChecked(e.getValue());
shareFoldersContainer.addView(checkBox);
presenter.bindingSubscriptions().add(RxCompoundButton.checkedChanges(checkBox)
.subscribe(checked -> {
presenter.setFolderShared(id, checked);
}));
}
}
public void saveDevice(View btn) {
boolean invalid = false;
invalid |= errorDeviceId != null;
invalid |= errorAddresses != 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) -> {
saveDevice();
})
.create());
} else {
saveDevice();
}
}
private void saveDevice() {
unsubscribe(saveSubscription);
onSaveStart();
saveSubscription = controller.editDevice(originalDevice, sharedFolders,
this::onSavefailed,
this::onSaveSuccessfull
);
}
public void deleteDevice(View btn) {
unsubscribe(deleteSubscription);
onSaveStart();
deleteSubscription = controller.deleteDevice(originalDevice,
this::onSavefailed,
this::onSaveSuccessfull
);
}
public void startQRScannerActivity(View btn) {
Intent intentScan = new Intent("com.google.zxing.client.android.SCAN");
intentScan.addCategory(Intent.CATEGORY_DEFAULT);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
try {
activityResultsController.startActivityForResult(intentScan, ActivityRequestCodes.SCAN_QR, null);
} catch (ActivityNotFoundException e) {
if (hasView()) {
dialogPresenter.showDialog(context -> new AlertDialog.Builder(context)
.setTitle(R.string.error)
.setMessage(R.string.no_qr_scanner_installed)
.setPositiveButton(android.R.string.ok, null)
.create());
}
}
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ActivityRequestCodes.SCAN_QR) {
if (resultCode == Activity.RESULT_OK && hasView()) {
String id = data.getStringExtra("SCAN_RESULT");
if (!StringUtils.isEmpty(id)) {
setDeviceID(id);
notifyChange(syncthing.android.BR.deviceID);
}
}
return true;
}
return false;
}
}