/* * 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.welcome; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.os.Bundle; import android.os.Handler; import android.os.Looper; 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.mortarfragment.FragmentManagerOwner; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import mortar.ViewPresenter; import rx.Observable; import rx.Scheduler; import rx.Subscriber; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.observers.Subscribers; import rx.schedulers.Schedulers; import syncthing.android.R; import syncthing.api.Credentials; import syncthing.android.service.ConfigXml; import syncthing.android.service.ServiceSettings; import syncthing.android.service.SyncthingInstance; import syncthing.android.service.SyncthingUtils; import syncthing.android.settings.AppSettings; import syncthing.android.ui.ManageActivity; import syncthing.android.ui.login.LoginFragment; import syncthing.api.SessionManager; import syncthing.api.SynchingApiWrapper; import syncthing.api.SyncthingApi; import syncthing.api.SyncthingApiConfig; import syncthing.api.model.Config; import syncthing.api.model.DeviceConfig; import syncthing.api.model.Ok; import timber.log.Timber; @ScreenScope public class WelcomePresenter extends ViewPresenter<WelcomeScreenView>{ class InitializedListener extends ContentObserver { public InitializedListener(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange) { onInstanceInitialized(); } } enum State { NONE, GENERATING, SUCCESS, ERROR, } final Context context; final AppSettings appSettings; final ActivityResultsController activityResultsController; final FragmentManagerOwner fragmentManager; final SessionManager manager; final ServiceSettings serviceSettings; final DialogPresenter dialogPresenter; final InitializedListener initializedListener = new InitializedListener(new Handler(Looper.getMainLooper())); final SyncthingApiConfig.Builder configBuilder = SyncthingApiConfig.builder(); final AtomicReference<Credentials> credentials = new AtomicReference<>(); int page = 0; State state = State.NONE; String error; Subscription subscription; Subscription splashSubscription; Subscriber<Boolean> initializedSubscriber; @Inject public WelcomePresenter( @ForApplication Context context, AppSettings appSettings, ActivityResultsController activityResultsController, FragmentManagerOwner fragmentManager, SessionManager manager, ServiceSettings serviceSettings, DialogPresenter dialogPresenter ) { this.context = context; this.appSettings = appSettings; this.activityResultsController = activityResultsController; this.fragmentManager = fragmentManager; this.manager = manager; this.serviceSettings = serviceSettings; this.dialogPresenter = dialogPresenter; } @Override protected void onExitScope() { super.onExitScope(); context.getContentResolver().unregisterContentObserver(initializedListener); if (splashSubscription != null) { splashSubscription.unsubscribe(); } unsubscribeS(); } @Override protected void onLoad(Bundle savedInstanceState) { super.onLoad(savedInstanceState); switch (state) { case NONE: delayHideSplash(); break; case SUCCESS: exitSuccess(); break; case ERROR: showError(); break; } } void reload() { if (!hasView()) return; getView().setPage(page); } void updatePage(int page) { this.page = page; } void delayHideSplash() { splashSubscription = Observable.timer(10, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) .subscribe(subscriber -> hideSplash()); } void hideSplash() { if (page != 0) return; if (!hasView()) return; getView().hideSplash(); } void unsubscribeS() { if (subscription != null) { final Subscription s = subscription; final Scheduler.Worker worker = Schedulers.io().createWorker(); worker.schedule(() -> { s.unsubscribe(); worker.unsubscribe(); }); } } /* * Im sure your looking at this and thinking for the love of god why * We use this rather complicated observable to fetch our credential information * and save a generated password all in one fail swoop. * The observable handles acquiring and releasing the session, (note we must acquire * two sessions so the post will succeed), the observable also handles retries. * By using 'defer' we create new OkHttp Calls on each retry, this is mandatory as * calls are oneshot deals. */ void onInstanceInitialized() { configBuilder.setUrl(SyncthingUtils.buildUrl(ConfigXml.LOCALHOST_IP, ConfigXml.DEFAULT_PORT, true)); subscription = Observable.using( () -> { Timber.d("Acquiring session"); return manager.acquire(configBuilder.build()); }, s -> { final SyncthingApi api = SynchingApiWrapper.wrap(s.api(), Schedulers.io()); return Observable.defer(() -> Observable.zip(api.system(), api.config(), (systemInfo, config) -> { //build our localhost credentials Credentials.Builder builder = Credentials.builder(); builder.setUrl(SyncthingUtils.buildUrl(ConfigXml.LOCALHOST_IP, ConfigXml.DEFAULT_PORT, true)); builder.setApiKey(config.gui.apiKey); builder.setId(systemInfo.myID); for (DeviceConfig d : config.devices) { if (StringUtils.equals(d.deviceID, systemInfo.myID)) { builder.setAlias(SyncthingUtils.getDisplayName(d)); break; } } builder.setCaCert(SyncthingUtils.getSyncthingCACert(context)); //save and set as default Credentials c = builder.build(); credentials.set(c); appSettings.saveCredentials(c); appSettings.setDefaultCredentials(c); //create random user/pass String username = SyncthingUtils.generateUsername(); String password = SyncthingUtils.generatePassword(); Timber.i("Generated GUI username and password (%s, %s)", username, password); config.gui.user = username; config.gui.password = password; return config; })).retryWhen(observable -> observable.flatMap(throwable -> { String msg = throwable.getMessage(); if (msg == null || !(msg.contains("ECONNREFUSED") || msg.contains("EOFException") || msg.contains("timeout") || msg.contains("Connection reset by peer"))) { // Another service running, propagate error and show to user Timber.d("Unknown error %s", msg); return Observable.error(throwable); } Timber.d("Retrying in 5 seconds"); return Observable.timer(5, TimeUnit.SECONDS); })); }, s -> { Timber.d("Releasing session"); manager.release(s); }, true // eagerly release ).observeOn(AndroidSchedulers.mainThread() ).flatMap(this::saveConfigObservable //want subscribe on main thread ).observeOn(AndroidSchedulers.mainThread() ).subscribe(Subscribers.create(this::finish, this::processError) //want results on main thread ); } Observable<Ok> saveConfigObservable(final Config config) { return Observable.using( () -> { Timber.d("Acquiring session2"); configBuilder.setApiKey(credentials.get().apiKey); configBuilder.setCaCert(credentials.get().caCert); return manager.acquire(configBuilder.build()); }, s -> { final SyncthingApi api = SynchingApiWrapper.wrap(s.api(), Schedulers.io()); return Observable.defer(() -> api.updateConfig(config) //restart .flatMap(c -> api.restart()) // Give syncthing a chance to reboot .delay(10, TimeUnit.SECONDS) ); }, s -> { Timber.d("Releasing session2"); manager.release(s); }, true // eagerly release );//TODO what kind of errors can be recovered from at this point } public void initializeInstance() { state = State.GENERATING; serviceSettings.setEnabled(true); context.startService(new Intent(context, SyncthingInstance.class).setAction(SyncthingInstance.REEVALUATE)); context.getContentResolver().registerContentObserver(serviceSettings.getInitializedUri(), false, initializedListener); } public void setInitializedSubscriber(Subscriber<Boolean> subscription) { initializedSubscriber = subscription; } void cancelGeneration() { unsubscribeS(); exitCanceled(); } void finish(Ok ok) { state = State.SUCCESS; Timber.d("Ready"); if (page == 1) this.page = 5; if (initializedSubscriber != null) { initializedSubscriber.onNext(true); } reload(); } void processError(Throwable t) { Timber.w("processError(%s)", t.getMessage()); if (!serviceSettings.isInitialised()) { serviceSettings.setEnabled(false); context.stopService(new Intent(context, SyncthingInstance.class)); throw new RuntimeException("Daemon failed to start"); } else { notifyError(t); } } void notifyError(Throwable t) { Timber.d("notifyError(%s)", t.getMessage()); state = State.ERROR; error = t.getMessage(); showError(); if (initializedSubscriber != null) { initializedSubscriber.onNext(false); } } void showError() { if (hasView()) { dialogPresenter.showDialog(context -> new AlertDialog.Builder(context) .setTitle(R.string.welcome_pl_generating_failed) .setMessage(error) .setPositiveButton(android.R.string.ok, null) .create() ); } } void exitSuccess() { Intent intent = new Intent().putExtra(ManageActivity.EXTRA_CREDENTIALS, credentials.get()); activityResultsController.setResultAndFinish(Activity.RESULT_OK, intent); } void exitCanceled() { fragmentManager.killBackStack(); fragmentManager.replaceMainContent(LoginFragment.newInstance(), false); } public State getState() { return state; } }