/*
* 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.login;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.databinding.Bindable;
import android.databinding.PropertyChangeRegistry;
import android.databinding.adapters.ViewBindingAdapter;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.design.widget.CoordinatorLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.Pair;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.apache.commons.lang3.StringUtils;
import org.opensilk.common.core.dagger2.ForApplication;
import org.opensilk.common.core.dagger2.ScreenScope;
import org.opensilk.common.core.rx.RxUtils;
import org.opensilk.common.ui.mortar.ActionBarConfig;
import org.opensilk.common.ui.mortar.ActivityResultsController;
import org.opensilk.common.ui.mortar.DialogPresenter;
import org.opensilk.common.ui.mortar.ToolbarOwner;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import mortar.ViewPresenter;
import rx.Scheduler;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import syncthing.android.R;
import syncthing.android.service.SyncthingUtils;
import syncthing.android.settings.AppSettings;
import syncthing.android.ui.ManageActivity;
import syncthing.android.ui.binding.BindingSubscriptionsHolder;
import syncthing.api.Credentials;
import syncthing.api.Session;
import syncthing.api.SessionManager;
import syncthing.api.SynchingApiWrapper;
import syncthing.api.SyncthingApi;
import syncthing.api.SyncthingApiConfig;
import syncthing.api.model.DeviceConfig;
import timber.log.Timber;
/**
* Created by drew on 3/11/15.
*/
@ScreenScope
public class LoginPresenter extends ViewPresenter<CoordinatorLayout> implements
android.databinding.Observable, BindingSubscriptionsHolder {
enum State {
NONE,
LOADING,
ERROR,
SUCCESS
}
final Context appContext;
final ActivityResultsController activityResultsController;
final AppSettings settings;
final SessionManager manager;
final DialogPresenter dialogPresenter;
final ToolbarOwner toolbarOwner;
final AtomicReference<Credentials> credentials = new AtomicReference<>();
final SyncthingApiConfig.Builder configBuilder = SyncthingApiConfig.builder();
final PropertyChangeRegistry registry = new PropertyChangeRegistry();
String alias = "";
String host = "";
String port = "8384";
String user = "";
String pass = "";
boolean tls;
String errorHost;
CompositeSubscription bindingSubscriptions;
Subscription subscription;
String error;
Session session;
State state = State.NONE;
boolean wasPreviouslyLoaded;
@Inject
public LoginPresenter(
@ForApplication Context context,
Credentials initialCredentials,
ActivityResultsController activityResultsController,
SessionManager manager,
AppSettings settings,
DialogPresenter dialogPresenter,
ToolbarOwner toolbarOwner
) {
this.appContext = context;
this.credentials.set(initialCredentials);
this.activityResultsController = activityResultsController;
this.settings = settings;
this.manager = manager;
if (initialCredentials != Credentials.NONE) {
configBuilder.forCredentials(initialCredentials);
}
this.dialogPresenter = dialogPresenter;
this.toolbarOwner = toolbarOwner;
}
@Override
protected void onExitScope() {
Timber.d("onExitScope");
super.onExitScope();
if (subscription != null) {
subscription.unsubscribe();
}
if (session != null) {
manager.release(session);
}
}
@Override
protected void onLoad(Bundle savedInstanceState) {
super.onLoad(savedInstanceState);
if (!wasPreviouslyLoaded && savedInstanceState != null) {
credentials.set(savedInstanceState.getParcelable("creds"));
} else if (!wasPreviouslyLoaded) {
if (credentials.get() != Credentials.NONE) {
Credentials c = credentials.get();
alias = c.alias;
host = SyncthingUtils.extractHost(c.url);
port = SyncthingUtils.extractPort(c.url);
tls = SyncthingUtils.isHttps(c.url);
}
}
switch (state) {
case ERROR:
showError();
break;
case LOADING:
showLoading();
break;
case SUCCESS:
exitSuccess();
break;
}
}
@Override
protected void onSave(Bundle outState) {
super.onSave(outState);
outState.putParcelable("creds", credentials.get());
}
@Override
public CompositeSubscription bindingSubscriptions() {
return (bindingSubscriptions != null) ? bindingSubscriptions : (bindingSubscriptions = new CompositeSubscription());
}
@Override
public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
registry.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
registry.remove(callback);
}
private void notifyChange(int id) {
registry.notifyChange(this, id);
}
public void submit(View btn) {
dismissKeyboard(btn);
if (isInputInvalid()) {
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) -> {
fetchApiKey();
})
.create());
} else {
fetchApiKey();
}
}
public void cancel(View btn) {
exitCanceled();
}
void fetchApiKey() {
if (!hasView()) return;
state = State.LOADING;
showLoading();
String url = SyncthingUtils.buildUrl(
host, //TODO undocumented behavior (default port)
StringUtils.isEmpty(port) ? (tls ? "443" : "80") : port,
tls
);
Timber.d("Logging into %s", url);
credentials.set(credentials.get().buildUpon().setUrl(url).build());
configBuilder.setUrl(url);
configBuilder.setAuth(SyncthingUtils.buildAuthorization(user, pass));
configBuilder.setDebug(true);
if (session != null) {
manager.release(session);
}
session = manager.acquire(configBuilder.build());
final SyncthingApi api = SynchingApiWrapper.wrap(session.api(), Schedulers.io());
subscription = rx.Observable.zip(api.config(), api.system(), Pair::create)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
pair -> {
Credentials.Builder builder = credentials.get().buildUpon();
builder.setId(pair.second.myID);
builder.setApiKey(pair.first.gui.apiKey);
if (StringUtils.isEmpty(alias)) {
for (DeviceConfig d : pair.first.devices) {
if (StringUtils.equals(d.deviceID, pair.second.myID)) {
builder.setAlias(SyncthingUtils.getDisplayName(d));
break;
}
}
} else {
builder.setAlias(alias);
}
credentials.set(builder.build());
},
this::notifyLoginError,
this::saveKeyAndFinish
);
}
void notifyLoginError(Throwable t) {
state = State.ERROR;
error = t.getMessage();
if (hasView()) {
showError();
}
}
void saveKeyAndFinish() {
state = State.SUCCESS;
settings.saveCredentials(credentials.get());
if (hasView()) {
exitSuccess();
}
}
void cancelLogin() {
if (subscription != null) {
final Subscription s = subscription;
subscription = null;
final Scheduler.Worker worker = Schedulers.io().createWorker();
worker.schedule(() -> {
s.unsubscribe();
worker.unsubscribe();
});
}
}
void showLoading() {
dialogPresenter.showDialog(
context -> {
ProgressDialog loadingProgress = new ProgressDialog(context);
loadingProgress.setMessage(context.getString(R.string.fetching_api_key_dots));
loadingProgress.setCancelable(false);
loadingProgress.setButton(DialogInterface.BUTTON_NEGATIVE,
context.getString(android.R.string.cancel),
(dialog, which) -> {
cancelLogin();
dialog.dismiss();
});
return loadingProgress;
}
);
}
void showError() {
dialogPresenter.showDialog(
context -> new AlertDialog.Builder(context)
.setTitle(R.string.login_failure)
.setMessage(error)
.setPositiveButton(android.R.string.ok, null)
.create());
}
void exitSuccess() {
Intent intent = new Intent().putExtra(ManageActivity.EXTRA_CREDENTIALS, (Parcelable) credentials.get());
activityResultsController.setResultAndFinish(Activity.RESULT_OK, intent);
}
public void exitCanceled() {
activityResultsController.setResultAndFinish(Activity.RESULT_CANCELED, new Intent());
}
void dismissKeyboard(View v) {
final InputMethodManager imm = (InputMethodManager)v.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
boolean isInputInvalid() {
boolean invalid = false;
invalid |= errorHost != null;
return invalid;
}
@Bindable
public String getAlias() {
return alias;
}
public final Action1<CharSequence> actionSetAlias = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
String s = StringUtils.isEmpty(charSequence) ? "" : charSequence.toString();
if (!StringUtils.equals(s, alias)) {
alias = s;
}
}
};
@Bindable
public String getHost() {
return host;
}
@Bindable
public String getErrorHost() {
return errorHost;
}
public final Action1<CharSequence> actionSetHost = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
String s = StringUtils.isEmpty(charSequence) ? "" : charSequence.toString();
boolean invalid = false;
if (StringUtils.isEmpty(s)) {
invalid = true;
}
if (hasView()) {
String err = (invalid ? getView().getContext().getString(R.string.input_error) : null);
if (!StringUtils.equals(err, errorHost)) {
errorHost = err;
notifyChange(syncthing.android.BR.errorHost);
}
}
if (!invalid && !StringUtils.equals(s, host)) {
host = s;
}
}
};
@Bindable
public String getPort() {
return port;
}
public final Action1<CharSequence> actionSetPort = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
String s = StringUtils.isEmpty(charSequence) ? "" : charSequence.toString();
if (!StringUtils.equals(s, port)) {
port = s;
}
}
};
@Bindable
public String getUser() {
return user;
}
public final Action1<CharSequence> actionSetUser = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
String s = StringUtils.isEmpty(charSequence) ? "" : charSequence.toString();
if (!StringUtils.equals(s, user)) {
user = s;
}
}
};
@Bindable
public String getPass() {
return pass;
}
public final Action1<CharSequence> actionSetPass = new Action1<CharSequence>() {
@Override
public void call(CharSequence charSequence) {
String s = StringUtils.isEmpty(charSequence) ? "" : charSequence.toString();
if (!StringUtils.equals(s, pass)) {
pass = s;
}
}
};
@Bindable
public boolean isTls() {
return tls;
}
public final Action1<Boolean> actionSetTls = new Action1<Boolean>() {
@Override
public void call(Boolean aBoolean) {
tls = aBoolean;
}
};
public final ViewBindingAdapter.OnViewAttachedToWindow toolbarAttachedListener =
new ViewBindingAdapter.OnViewAttachedToWindow() {
@Override
public void onViewAttachedToWindow(View v) {
Timber.d("attachingtoolbar");
Toolbar toolbar = (Toolbar)v;
toolbarOwner.attachToolbar(toolbar);
toolbarOwner.setConfig(ActionBarConfig.builder().setTitle(R.string.login).build());
}
};
public final ViewBindingAdapter.OnViewDetachedFromWindow toolbarDetachedListener =
new ViewBindingAdapter.OnViewDetachedFromWindow() {
@Override
public void onViewDetachedFromWindow(View v) {
Timber.d("detachingToolbar");
Toolbar toolbar = (Toolbar) v;
toolbarOwner.detachToolbar(toolbar);
}
};
public final ViewBindingAdapter.OnViewDetachedFromWindow dropViewListener =
new ViewBindingAdapter.OnViewDetachedFromWindow() {
@Override
public void onViewDetachedFromWindow(View v) {
Timber.d("Dropping view %s");
dropView((CoordinatorLayout) v);
RxUtils.unsubscribe(bindingSubscriptions);
bindingSubscriptions = null;
}
};
}