/*
* 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.Context;
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.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.Filter;
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.ActionBarConfig;
import org.opensilk.common.ui.mortar.ActionBarMenuConfig;
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 org.opensilk.common.ui.mortarfragment.FragmentManagerOwner;
import java.util.ArrayList;
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.ManageActivity;
import syncthing.android.ui.common.ActivityRequestCodes;
import syncthing.android.ui.folderpicker.FolderPickerFragment;
import syncthing.api.SessionManager;
import syncthing.api.model.DeviceConfig;
import syncthing.api.model.FolderConfig;
import syncthing.api.model.FolderDeviceConfig;
import syncthing.api.model.PullOrder;
import syncthing.api.model.SystemInfo;
import syncthing.api.model.Version;
import syncthing.api.model.VersioningExternal;
import syncthing.api.model.VersioningNone;
import syncthing.api.model.VersioningSimple;
import syncthing.api.model.VersioningStaggered;
import syncthing.api.model.VersioningTrashCan;
import syncthing.api.model.VersioningType;
import timber.log.Timber;
import static syncthing.android.ui.sessionsettings.EditPresenterConfig.INVALID_ID;
/**
* Created by drew on 3/16/15.
*/
@ScreenScope
public class EditFolderPresenter extends EditPresenter<CoordinatorLayout>
implements ActivityResultsListener, android.databinding.DataBindingComponent {
final FragmentManagerOwner fm;
FolderConfig origFolder;
VersioningTrashCan.Params trashCanParams = new VersioningTrashCan.Params();
VersioningSimple.Params simpleParams = new VersioningSimple.Params();
VersioningStaggered.Params staggeredParams = new VersioningStaggered.Params();
VersioningExternal.Params externalParams = new VersioningExternal.Params();
boolean newShare;
TreeMap<String, Boolean> sharedDevices;
Subscription deleteSubscription;
String errorFolderId;
String errorFolderPath;
String errorRescanInterval;
String errorTrashCanParamCleanoutDays;
String errorSimpleParamKeep;
String errorStaggeredParamMaxAge;
String errorExternalParamCmd;
@Inject
public EditFolderPresenter(
SessionManager manager,
DialogPresenter dialogPresenter,
ActivityResultsController activityResultContoller,
ToolbarOwner toolbarOwner,
EditPresenterConfig config,
FragmentManagerOwner fm
) {
super(manager, dialogPresenter, activityResultContoller, toolbarOwner, config);
this.fm = fm;
}
@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) {
origFolder = (FolderConfig) savedInstanceState.getSerializable("folder");
newShare = savedInstanceState.getBoolean("newShare");
sharedDevices = (TreeMap<String, Boolean>) savedInstanceState.getSerializable("sharedDevices");
initParams(origFolder);
} else if (!wasPreviouslyLoaded) {
if (isAdd) {
origFolder = FolderConfig.withDefaults();
if (!INVALID_ID.equals(folderId)) {
origFolder.id = folderId;
newShare = true;
}
SystemInfo sys = controller.getSystemInfo();
if (sys != null) {
origFolder.path = sys.tilde;
}
Version ver = controller.getVersion();
if (ver != null && StringUtils.equals(ver.os, "android")) {
//set ignore perms on android by default
origFolder.ignorePerms = true;
}
} else {
FolderConfig f = controller.getFolder(folderId);
if (f != null) {
origFolder = f.clone();
}
}
initParams(origFolder);
sharedDevices = new TreeMap<>();
List<DeviceConfig> devices = controller.getRemoteDevices();
for (DeviceConfig d : devices) {
sharedDevices.put(d.deviceID, false);
if (origFolder != null && origFolder.devices != null) {
for (FolderDeviceConfig d2 : origFolder.devices) {
if (StringUtils.equals(d2.deviceID, d.deviceID)) {
sharedDevices.put(d.deviceID, true);
break;
}
}
}
}
if (!INVALID_ID.equals(deviceId) && sharedDevices.containsKey(deviceId)) {
sharedDevices.put(deviceId, true);
}
}
wasPreviouslyLoaded = true;
if (origFolder == null) {
Timber.e("Folder was null! cannot continue.");
dismissDialog();
}
}
@Override
protected void onSave(Bundle outState) {
super.onSave(outState);
outState.putSerializable("folder", origFolder);
outState.putBoolean("newShare", newShare);
outState.putSerializable("sharedDevices", sharedDevices);
}
@Override
public ActionBarConfig getToolbarConfig() {
if (isAdd) {
return super.getToolbarConfig();
}
return super.getToolbarConfig().buildUpon()
.setMenuConfig(ActionBarMenuConfig.builder()
.withMenu(R.menu.folder_ignores)
.setActionHandler((context, id) -> {
switch (id) {
case R.id.edit_ignores:
openIgnoresEditor(context);
return true;
default:
return false;
}
}).build()).build();
}
@Bindable
public boolean isAdd() {
return isAdd;
}
@Bindable
public boolean isNewShare() {
return newShare;
}
@Bindable
public String getFolderID() {
return origFolder.id;
}
public void setFolderID(CharSequence text) {
if (!isAdd || newShare) {
return;
}
if (validateFolderId(text)) {
origFolder.id = text.toString();
}
}
public final Action1<CharSequence> actionSetFolderID = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setFolderID(charSequence);
}
};
@Bindable
public String getFolderIDError() {
return errorFolderId;
}
public void setFolderIDError(String text) {
if (!StringUtils.equals(errorFolderId, text)) {
errorFolderId = text;
notifyChange(syncthing.android.BR.folderIDError);
}
}
boolean validateFolderId(CharSequence text) {
int e = 0;
if (StringUtils.isEmpty(text.toString())) {
e = R.string.the_folder_id_cannot_be_blank;
} else if (!isFolderIdUnique(text.toString(), controller.getFolders())) {
e = R.string.the_folder_id_must_be_unique;
}
if (hasView()) {
setFolderIDError(e != 0 ? getView().getContext().getString(e) : null);
}
return e == 0;
}
static boolean isFolderIdUnique(CharSequence text, Collection<FolderConfig> folders) {
for (FolderConfig f : folders) if (StringUtils.equals(f.id, text)) return false;
return true;
}
@Bindable
public String getFolderPath() {
return origFolder.path;
}
public void setFolderPath(CharSequence text) {
if (validateFolderPath(text)) {
origFolder.path = text.toString();
}
}
public final Action1<CharSequence> actionSetFolderPath = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setFolderPath(charSequence);
}
};
@Bindable
public String getFolderPathError() {
return errorFolderPath;
}
public void setFolderPathError(String text) {
if (!StringUtils.equals(errorFolderPath, text)) {
errorFolderPath = text;
notifyChange(syncthing.android.BR.folderPathError);
}
}
boolean validateFolderPath(CharSequence text) {
boolean invalid = false;
if (StringUtils.isEmpty(text)) {
invalid = true;
}
if (hasView()) {
setFolderPathError(invalid ? getView().getContext().getString(R.string.the_folder_path_cannot_be_blank) : null);
}
return !invalid;
}
@Bindable
public String getRescanInterval() {
return String.valueOf(origFolder.rescanIntervalS);
}
public void setRescanInterval(CharSequence text) {
if (validateRescanInterval(text)) {
origFolder.rescanIntervalS = Integer.valueOf(text.toString());
}
}
public final Action1<CharSequence> actionSetRescanInterval = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setRescanInterval(charSequence);
}
};
@Bindable
public String getRescanIntervalError() {
return errorRescanInterval;
}
public void setRescanIntervalError(String text) {
if (!StringUtils.equals(errorRescanInterval, text)) {
errorRescanInterval = text;
notifyChange(syncthing.android.BR.rescanIntervalError);
}
}
boolean validateRescanInterval(CharSequence text) {
boolean invalid = false;
//input disallows negative numbers and non numerals;
if (StringUtils.isEmpty(text)) {
invalid = true;
}
if (hasView()) {
setRescanIntervalError(invalid ? getView().getContext().getString(R.string.the_rescan_interval_must_be_a_nonnegative_number_of_seconds) : null);
}
return !invalid;
}
@Bindable
public boolean isReadOnly() {
return origFolder.readOnly;
}
public void setReadOnly(boolean readOnly) {
origFolder.readOnly = readOnly;
}
public final Action1<Boolean> actionSetReadOnly = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setReadOnly(aBoolean);
}
};
@Bindable
public boolean isIgnorePerms() {
return origFolder.ignorePerms;
}
public void setIgnorePerms(boolean ignorePerms) {
origFolder.ignorePerms = ignorePerms;
}
public final Action1<Boolean> actionSetIgnorePerms = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
setIgnorePerms(aBoolean);
}
};
@Bindable
public PullOrder getPullOrder() {
return origFolder.order;
}
public void setPullOrder(PullOrder order) {
origFolder.order = order;
}
public final Action1<Integer> actionOnPullOrderChanged = new Action1<Integer>() {
@Override
public void call(Integer checkedId) {
switch (checkedId) {
case R.id.radio_pullorder_alphabetic:
setPullOrder(PullOrder.ALPHABETIC);
break;
case R.id.radio_pullorder_smallestfirst:
setPullOrder(PullOrder.SMALLESTFIRST);
break;
case R.id.radio_pullorder_largestfirst:
setPullOrder(PullOrder.LARGESTFIRST);
break;
case R.id.radio_pullorder_oldestfirst:
setPullOrder(PullOrder.OLDESTFIRST);
break;
case R.id.radio_pullorder_newestfirst:
setPullOrder(PullOrder.NEWESTFIRST);
break;
case R.id.radio_pullorder_random:
setPullOrder(PullOrder.RANDOM);
break;
}
}
};
@Bindable
public VersioningType getVersioningType() {
return origFolder.versioning.type;
}
public void setVersioningType(VersioningType type) {
switch (type) {
case TRASHCAN:
origFolder.versioning = new VersioningTrashCan(type, trashCanParams);
break;
case SIMPLE:
origFolder.versioning = new VersioningSimple(type, simpleParams);
break;
case STAGGERED:
origFolder.versioning = new VersioningStaggered(type, staggeredParams);
break;
case EXTERNAL:
origFolder.versioning = new VersioningExternal(type, externalParams);
break;
case NONE:
origFolder.versioning = new VersioningNone(type);
break;
}
notifyChange(syncthing.android.BR.versioningType);
}
public final Action1<Integer> actionOnVersioningChanged = new Action1<Integer>() {
@Override
public void call(Integer checkedId) {
switch (checkedId) {
case R.id.radio_trashcan_versioning:
setVersioningType(VersioningType.TRASHCAN);
break;
case R.id.radio_simple_versioning:
setVersioningType(VersioningType.SIMPLE);
break;
case R.id.radio_staggered_versioning:
setVersioningType(VersioningType.STAGGERED);
break;
case R.id.radio_external_versioning:
setVersioningType(VersioningType.EXTERNAL);
break;
case R.id.radio_no_versioning:
setVersioningType(VersioningType.NONE);
break;
}
}
};
private void initParams(FolderConfig f) {
if (f == null || f.versioning == null) return;
switch (origFolder.versioning.type) {
case TRASHCAN:
trashCanParams = (VersioningTrashCan.Params) origFolder.versioning.params;
break;
case SIMPLE:
simpleParams = (VersioningSimple.Params) origFolder.versioning.params;
break;
case STAGGERED:
staggeredParams = (VersioningStaggered.Params) origFolder.versioning.params;
break;
case EXTERNAL:
externalParams = (VersioningExternal.Params) origFolder.versioning.params;
break;
}
}
@Bindable
public VersioningTrashCan.Params getTrashCanParams() {
return trashCanParams;
}
public void setTrashCanParamCleanDays(CharSequence text) {
if (validateTrashCanVersioningCleanoutDays(text)) {
trashCanParams.cleanoutDays = text.toString();
}
}
public final Action1<CharSequence> actionSetTrashCanParamCleanoutDays = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setTrashCanParamCleanDays(charSequence);
}
};
@Bindable
public String getTrashCanParamCleanoutDaysError() {
return errorTrashCanParamCleanoutDays;
}
public void setTrashCanParamCleanoutDaysError(String text) {
if (!StringUtils.equals(errorTrashCanParamCleanoutDays, text)) {
errorTrashCanParamCleanoutDays = text;
notifyChange(syncthing.android.BR.trashCanParamCleanoutDaysError);
}
}
boolean validateTrashCanVersioningCleanoutDays(CharSequence text) {
boolean invalid = false;
//input disallows negative numbers and non numerals;
if (StringUtils.isEmpty(text)) {
invalid = true;
}
if (hasView()) {
setTrashCanParamCleanoutDaysError(invalid ? getView().getContext().getString(R.string.the_number_of_days_must_be_a_number_and_cannot_be_blank) : null);
}
return !invalid;
}
@Bindable
public VersioningSimple.Params getSimpleParams() {
return simpleParams;
}
public void setSimpleParamKeep(CharSequence text) {
if (validateSimpleVersioningKeep(text)) {
simpleParams.keep = text.toString();
}
}
public final Action1<CharSequence> actionSetSimpleParamKeep = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setSimpleParamKeep(charSequence);
}
};
@Bindable
public String getSimpleParamKeepError() {
return errorSimpleParamKeep;
}
public void setSimpleParamKeepError(String text) {
if (!StringUtils.equals(errorSimpleParamKeep, text)) {
errorSimpleParamKeep = text;
notifyChange(syncthing.android.BR.simpleParamKeepError);
}
}
boolean validateSimpleVersioningKeep(CharSequence text) {
int e = 0;
//input disallows negative numbers and non numerals;
if (StringUtils.isEmpty(text)) {
e = R.string.the_number_of_versions_must_be_a_number_and_cannot_be_blank;
} else if (Integer.parseInt(text.toString()) == 0) {
e = R.string.you_must_keep_at_least_one_version;
}
if (hasView()) {
setSimpleParamKeepError(e != 0 ? getView().getContext().getString(e) : null);
}
return e == 0;
}
@Bindable
public VersioningStaggered.Params getStaggeredParams() {
return staggeredParams;
}
@Bindable
public String getStaggeredParamMaxAge() {
return SyncthingUtils.secondsToDays(staggeredParams.maxAge);
}
public void setStaggeredParamMaxAge(CharSequence text) {
if (validateStaggeredMaxAge(text)) {
staggeredParams.maxAge = SyncthingUtils.daysToSeconds(text.toString());
}
}
public final Action1<CharSequence> actionSetStaggeredParamMaxAge = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setStaggeredParamMaxAge(charSequence);
}
};
@Bindable
public String getStaggeredParamMaxAgeError(){
return errorStaggeredParamMaxAge;
}
public void setStaggeredParamMaxAgeError(String text) {
if (!StringUtils.equals(errorStaggeredParamMaxAge, text)) {
errorStaggeredParamMaxAge = text;
notifyChange(syncthing.android.BR.staggeredParamMaxAgeError);
}
}
boolean validateStaggeredMaxAge(CharSequence text) {
boolean invalid = false;
//input disallows negative numbers and non numerals;
if (StringUtils.isEmpty(text)) {
invalid = true;
}
if (hasView()) {
setStaggeredParamMaxAgeError(invalid ? getView().getContext().getString(R.string.the_maximum_age_must_be_a_number_and_cannot_be_blank) : null);
}
return !invalid;
}
public void setStaggeredParamPath(CharSequence text) {
staggeredParams.versionPath = StringUtils.isEmpty(text) ? "" : text.toString();
}
public final Action1<CharSequence> actionSetStaggeredParamPath = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setStaggeredParamPath(charSequence);
}
};
@Bindable
public VersioningExternal.Params getExternalParams() {
return externalParams;
}
public void setExternalParamCmd(CharSequence text) {
if (validateExternalVersioningCmd(text)) {
externalParams.command = text.toString();
}
}
public final Action1<CharSequence> actionSetExternalParamCmd = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
setExternalParamCmd(charSequence);
}
};
@Bindable
public String getExternalParamCmdError() {
return errorExternalParamCmd;
}
public void setExternalParamCmdError(String text) {
if (!StringUtils.equals(errorExternalParamCmd, text)) {
errorExternalParamCmd = text;
notifyChange(syncthing.android.BR.externalParamCmdError);
}
}
boolean validateExternalVersioningCmd(CharSequence text) {
boolean invalid = false;
if (StringUtils.isEmpty(text)) {
invalid = true;
}
if (hasView()) {
setExternalParamCmdError(invalid ? getView().getContext().getString(R.string.the_path_cannot_be_blank) : null);
}
return !invalid;
}
public void setDeviceShared(String id, boolean shared) {
sharedDevices.put(id, shared);
}
@BindingAdapter("addShareDevices")
public static void addShareDevices(LinearLayout shareDevicesContainer, EditFolderPresenter presenter) {
if (presenter == null) return;
shareDevicesContainer.removeAllViews();
for (Map.Entry<String, Boolean> e : presenter.sharedDevices.entrySet()) {
final String id = e.getKey();
CheckBox checkBox = new CheckBox(shareDevicesContainer.getContext());
DeviceConfig device = presenter.controller.getDevice(id);
if (device == null) {
device = new DeviceConfig();
device.deviceID = id;
}
checkBox.setText(SyncthingUtils.getDisplayName(device));
checkBox.setChecked(e.getValue());
shareDevicesContainer.addView(checkBox);
presenter.bindingSubscriptions().add(RxCompoundButton.checkedChanges(checkBox)
.subscribe(b -> {
presenter.setDeviceShared(id, b);
}));
}
}
public void saveFolder(View btn) {
boolean invalid = false;
invalid |= errorFolderId != null;
invalid |= errorFolderPath != null;
invalid |= errorRescanInterval != null;
switch (getVersioningType()) {
case TRASHCAN:
invalid |= errorTrashCanParamCleanoutDays != null;
break;
case STAGGERED:
invalid |= errorStaggeredParamMaxAge != null;
break;
case SIMPLE:
invalid |= errorSimpleParamKeep != null;
break;
case EXTERNAL:
invalid |= errorExternalParamCmd != null;
break;
}
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) -> {
saveFolder();
})
.create());
} else {
saveFolder();
}
}
private void saveFolder() {
List<FolderDeviceConfig> devices = new ArrayList<>();
for (Map.Entry<String, Boolean> e : sharedDevices.entrySet()) {
if (e.getValue()) {
devices.add(new FolderDeviceConfig(e.getKey()));
}
}
origFolder.devices = devices;
unsubscribe(saveSubscription);
onSaveStart();
saveSubscription = controller.editFolder(origFolder,
this::onSavefailed,
this::onSaveSuccessfull
);
}
public void deleteFolder(View btn) {
unsubscribe(deleteSubscription);
onSaveStart();
deleteSubscription = controller.deleteFolder(origFolder,
this::onSavefailed,
this::onSaveSuccessfull
);
}
public void openIgnoresEditor(Context context) {
EditIgnoresFragment f = EditIgnoresFragment.ni(context, credentials, folderId);
fm.replaceMainContent(f, true);
}
public void openFolderPicker(View btn) {
String path = getFolderPath();
if (StringUtils.endsWith(path, "/")) {
path = path.substring(0, path.length() - 1);
} else if (path.lastIndexOf("/") > 0) {
//we want the last directory they inputed not any partial name in there
path = path.substring(0, path.lastIndexOf("/"));
}
Intent i = new Intent(btn.getContext(), ManageActivity.class)
.putExtra(ManageActivity.EXTRA_FRAGMENT, FolderPickerFragment.NAME)
.putExtra(ManageActivity.EXTRA_ARGS, FolderPickerFragment.makeArgs(credentials, path))
.putExtra(ManageActivity.EXTRA_UP_IS_BACK, true);
activityResultsController.startActivityForResult(i, ActivityRequestCodes.FOLDER_PICKER, null);
}
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == ActivityRequestCodes.FOLDER_PICKER) {
if (resultCode == Activity.RESULT_OK) {
String path = data.getStringExtra("path");
if (path != null) {
setFolderPath(path);
notifyChange(syncthing.android.BR.folderPath);
}
}
return true;
} else {
return false;
}
}
static class DirectoryAutoCompleteAdapter extends ArrayAdapter<String> {
final EditFolderPresenter presenter;
DirectoryAutoCompleteAdapter(Context context, EditFolderPresenter presenter) {
super(context, android.R.layout.simple_dropdown_item_1line);
this.presenter = presenter;
setNotifyOnChange(false);
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
try {
List<String> results = presenter.controller
.getAutoCompleteDirectoryList(constraint.toString())
.toBlocking().first();
FilterResults fr = new FilterResults();
fr.values = results;
fr.count = results.size();
return fr;
} catch (Exception e) { //cant remember what in throws
FilterResults fr = new FilterResults();
fr.values = new ArrayList<String>();
fr.count = 0;
return fr;
}
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results.count == 0) {
clear();
notifyDataSetInvalidated();
} else {
clear();
addAll((List<String>)results.values);
notifyDataSetChanged();
}
}
};
}
}
}