/* * 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.session; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; import android.support.v7.app.AlertDialog; import android.widget.Toast; 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.ActionBarConfig; import org.opensilk.common.ui.mortar.ActivityResultsController; import org.opensilk.common.ui.mortar.DialogPresenter; import org.opensilk.common.ui.mortar.Lifecycle; import org.opensilk.common.ui.mortar.LifecycleService; import org.opensilk.common.ui.mortarfragment.FragmentManagerOwner; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import mortar.MortarScope; import mortar.Presenter; import mortar.bundler.BundleService; import rx.Subscription; import rx.functions.Action1; import syncthing.android.R; import syncthing.android.identicon.IdenticonComponent; import syncthing.android.identicon.IdenticonGenerator; import syncthing.android.service.SyncthingUtils; import syncthing.android.ui.ManageActivity; import syncthing.android.ui.common.ActivityRequestCodes; import syncthing.android.ui.sessionsettings.EditDeviceFragment; import syncthing.android.ui.sessionsettings.EditFolderFragment; import syncthing.android.ui.sessionsettings.EditIgnoresFragment; import syncthing.android.ui.sessionsettings.SettingsFragment; import syncthing.api.Credentials; import syncthing.api.Session; import syncthing.api.SessionController; import syncthing.api.SessionManager; import syncthing.api.model.ConnectionInfo; import syncthing.api.model.DeviceConfig; import syncthing.api.model.DeviceStats; import syncthing.api.model.FolderConfig; import syncthing.api.model.Model; import syncthing.api.model.SystemInfo; import syncthing.api.model.SystemMessage; import syncthing.api.model.Version; import syncthing.api.model.event.DeviceRejected; import syncthing.api.model.event.FolderCompletion; import syncthing.api.model.event.FolderErrors; import syncthing.api.model.event.FolderRejected; import syncthing.api.model.event.FolderScanProgress; import syncthing.api.model.event.FolderSummary; import syncthing.api.model.event.StateChanged; import timber.log.Timber; /** * Created by drew on 3/11/15. */ @ScreenScope public class SessionPresenter extends Presenter<ISessionScreenView> implements android.databinding.DataBindingComponent, IdenticonComponent { final Context appContext; final Credentials credentials; final SessionController controller; final FragmentManagerOwner fragmentManagerOwner; final IdenticonGenerator identiconGenerator; final ActivityResultsController activityResultsController; final Session session; final SessionManager manager; final DialogPresenter dialogPresenter; Subscription changeSubscription; final ArrayList<NotifCard> notifications = new ArrayList<>(); final ArrayList<FolderCard> folders = new ArrayList<>(); final ArrayList<DeviceCard> devices = new ArrayList<>(); MyDeviceCard myDevice; Subscription lifecycleSubscription; @Inject public SessionPresenter( @ForApplication Context appContext, Credentials credentials, SessionManager manager, FragmentManagerOwner fragmentManagerOwner, IdenticonGenerator identiconGenerator, ActivityResultsController activityResultsController, DialogPresenter dialogPresenter ) { this.appContext = appContext; this.credentials = credentials; this.fragmentManagerOwner = fragmentManagerOwner; this.identiconGenerator = identiconGenerator; this.activityResultsController = activityResultsController; this.session = manager.acquire(credentials); this.manager = manager; this.controller = session.controller(); this.dialogPresenter = dialogPresenter; } @Override protected BundleService extractBundleService(ISessionScreenView view) { return BundleService.getBundleService(view.getContext()); } @Override protected void onEnterScope(MortarScope scope) { Timber.d("onEnterScope"); super.onEnterScope(scope); changeSubscription = controller.subscribeChanges(this::onChange); lifecycleSubscription = LifecycleService.getLifecycle(scope) .subscribe(new Action1<Lifecycle>() { @Override public void call(Lifecycle lifecycle) { switch (lifecycle) { case RESUME: Timber.d("Initializing controller"); controller.init(); break; case PAUSE: Timber.d("Suspending controller"); controller.suspend(); break; } } }); } @Override protected void onExitScope() { Timber.d("onExitScope"); super.onExitScope(); if (changeSubscription != null) { changeSubscription.unsubscribe(); } if (lifecycleSubscription != null) { lifecycleSubscription.unsubscribe(); } manager.release(session); } @Override protected void onLoad(Bundle savedInstanceState) { super.onLoad(savedInstanceState); if (controller.isOnline()) { initializeView(); getView().showList(false); dismissRestartingDialog(); } else if (controller.isRestarting()) { showRestartingDialog(); } else /*offline*/ { getView().showLoading(); } } @Override protected void onSave(Bundle outState) { super.onSave(outState); } public boolean isSessionValid() { return controller.isOnline() && controller.getSystemInfo() != null; } void onChange(SessionController.ChangeEvent e) { SessionController.Change change = e.change; Timber.d("onChange(%s)", change.toString()); switch (change) { case ONLINE: if (hasView()) { initializeView(); getView().showList(true); dismissRestartingDialog(); getView().updateToolbarState(true); } break; case OFFLINE: if (hasView()) { if (controller.isRestarting()) { showRestartingDialog(); } else { getView().showLoading(); } getView().updateToolbarState(false); } break; case FAILURE: if (hasView()) { //controller has given up getView().showEmpty(true); dismissRestartingDialog(); } break; case DEVICE_REJECTED: case FOLDER_REJECTED: case NOTICE: updateNotifications(); if (hasView()) { getView().refreshNotifications(notifications); } break; case CONFIG_UPDATE: updateNotifications(); updateFolders(); updateDevices(); updateThisDevice(); if (hasView()) { getView().refreshNotifications(notifications); getView().refreshFolders(folders); getView().refreshDevices(devices); getView().refreshThisDevice(myDevice); } break; case NEED_LOGIN: openLoginScreen(); break; case COMPLETION: onCompletionUpdate(e.data); break; case CONNECTIONS_UPDATE: case CONNECTIONS_CHANGE: case DEVICE_PAUSED: case DEVICE_RESUMED: postConnectiosUpdate(); break; case DEVICE_STATS: postDeviceStatsUpdate(); break; case SYSTEM: onSystemInfoUpdate(); break; case FOLDER_SUMMARY: onFolderModelUpdate(e.data); break; case STATE_CHANGED: onFolderStateChange(e.data); break; case FOLDER_STATS: postFolderStatsUpdate(); break; case FOLDER_ERRORS: { FolderErrors.Data d = (FolderErrors.Data) e.data; onFolderErrors(d); break; } case FOLDER_SCAN_PROGRESS: { FolderScanProgress.Data d = (FolderScanProgress.Data) e.data; onFolderScanProgress(d); break; } default: break; } } void initializeView() { if (!hasView()) throw new IllegalStateException("initialize called without view"); updateNotifications(); updateFolders(); updateThisDevice(); updateDevices(); getView().initialize( notifications, folders, myDevice, devices ); } void updateNotifications() { boolean hasResartNotif = false; boolean hasErrorNotif = false; final boolean configInSync = controller.isConfigInSync(); final SystemMessage lastError = controller.getLatestError(); final Set<Map.Entry<String, DeviceRejected>> deviceRejs = controller.getDeviceRejections(); final Set<Map.Entry<String, FolderRejected>> folderRejs = controller.getFolderRejections(); Iterator<NotifCard> ni = notifications.iterator(); while (ni.hasNext()) { NotifCard n = ni.next(); switch (n.getKind()) { case RESTART: { if (!configInSync) { hasResartNotif = true; } else { ni.remove(); } break; } case ERROR: { if (lastError != null) { NotifCardError ne = (NotifCardError) n; ne.setError(lastError); hasErrorNotif = true; } else { ni.remove(); } break; } case DEVICE_REJ: { NotifCardRejDevice nd = (NotifCardRejDevice) n; Iterator<Map.Entry<String, DeviceRejected>> di = deviceRejs.iterator(); boolean found = false; while (di.hasNext()) { Map.Entry<String, DeviceRejected> e = di.next(); if (StringUtils.equals(nd.getKey(), e.getKey())) { di.remove(); found = true; break; } } if (!found) { ni.remove(); } break; } case FOLDER_REJ: { NotifCardRejFolder nf = (NotifCardRejFolder) n; Iterator<Map.Entry<String, FolderRejected>> fi = folderRejs.iterator(); boolean found = false; while (fi.hasNext()) { Map.Entry<String, FolderRejected> e = fi.next(); if (StringUtils.equals(nf.getKey(), e.getKey())) { fi.remove(); found = true; break; } } if (!found) { ni.remove(); } break; } } } if (!configInSync && !hasResartNotif) { notifications.add(new NotifCardRestart(this)); } if (lastError != null && !hasErrorNotif) { notifications.add(new NotifCardError(this, lastError)); } for (Map.Entry<String, DeviceRejected> e : deviceRejs) { notifications.add(new NotifCardRejDevice(this, e.getKey(), e.getValue())); } for (Map.Entry<String, FolderRejected> e : folderRejs) { notifications.add(new NotifCardRejFolder(this, e.getKey(), e.getValue())); } Collections.sort(notifications); } void updateFolders() { List<FolderConfig> folderConfigs = controller.getFolders(); if (folderConfigs.size() > 0) { Set<String> configIds = new HashSet<>(folderConfigs.size()); for (FolderConfig c : folderConfigs) { configIds.add(c.id); } //remove any old ones Iterator<FolderCard> ic = folders.iterator(); while (ic.hasNext()) { if(!configIds.contains(ic.next().getId())) { ic.remove(); } } } else { //remove all folders.clear(); return; } List<String> needsUpdate = new ArrayList<>(); for (FolderConfig folder : folderConfigs) { Model model = controller.getModel(folder.id); FolderCard card = getFolderCard(folder.id); if (card == null) { card = new FolderCard(this, folder, model); folders.add(card); } else { card.setFolder(folder); card.setModel(model); } if (model == null) { needsUpdate.add(folder.id); continue; } card.setScanProgress(controller.getFolderScanProgress(folder.id)); card.setErrors(controller.getFolderErrors(folder.id)); card.setStats(controller.getFolderStats(folder.id)); } if (!needsUpdate.isEmpty()) { controller.refreshFolders(needsUpdate); } Collections.sort(folders, (lhs, rhs) -> lhs.getId().compareTo(rhs.getId())); } void updateFoldersAndNotify() { updateFolders(); if (hasView()) { getView().refreshFolders(folders); } } private FolderCard getFolderCard(String id) { for (FolderCard fc : folders) { if (StringUtils.equals(fc.getId(), id)) { return fc; } } return null; } void updateThisDevice() { DeviceConfig device = controller.getThisDevice(); ConnectionInfo conn = controller.getConnectionTotal(); SystemInfo sys = controller.getSystemInfo(); Version ver = controller.getVersion(); if (myDevice == null) { myDevice = new MyDeviceCard(device, conn, sys, ver); } else { myDevice.setConnectionInfo(conn); myDevice.setSystemInfo(sys); } } void updateThisDeviceAndNotify() { updateThisDevice(); if (hasView()) { getView().refreshThisDevice(myDevice); } } void updateDevices() { Collection<DeviceConfig> remoteDevices = controller.getRemoteDevices(); if (devices.size() >0 ) { Set<String> deviceIds = new HashSet<>(remoteDevices.size()); for (DeviceConfig c : remoteDevices) { deviceIds.add(c.deviceID); } //remove any old ones Iterator<DeviceCard> ic = devices.iterator(); while (ic.hasNext()) { if (!deviceIds.contains(ic.next().getDeviceID())) { ic.remove(); } } } for (DeviceConfig device : remoteDevices) { ConnectionInfo connection = controller.getConnection(device.deviceID); DeviceStats stats = controller.getDeviceStats(device.deviceID); int completion = controller.getCompletionTotal(device.deviceID); DeviceCard c = getDeviceCard(device.deviceID); if (c != null) { c.setDevice(device); c.setConnectionInfo(connection); c.setDeviceStats(stats); c.setCompletion(completion); } else { devices.add(new DeviceCard(this, device, connection, stats, completion)); } } Collections.sort(devices, (lhs, rhs) -> lhs.getDeviceID().compareTo(rhs.getDeviceID())); } void updateDevicesAndNotify() { updateDevices(); if (hasView()) { getView().refreshDevices(devices); } } private DeviceCard getDeviceCard(String id) { for (DeviceCard c : devices) { if (StringUtils.equals(c.device.deviceID, id)) { return c; } } return null; } void onCompletionUpdate(Object o) { if (SessionController.ChangeEvent.NONE == o) { for (DeviceCard c : devices) { c.setCompletion(controller.getCompletionTotal(c.getDeviceID())); } } else { FolderCompletion.Data data = (FolderCompletion.Data) o; DeviceCard c = getDeviceCard(data.device); if (c != null) { c.setCompletion(controller.getCompletionTotal(c.getDeviceID())); } else { updateDevicesAndNotify(); } } } //TODO only notify on changed device void postConnectiosUpdate() { for (DeviceCard c : devices) { ConnectionInfo conn = controller.getConnection(c.getDeviceID()); c.setConnectionInfo(conn); } ConnectionInfo tConn = controller.getConnectionTotal(); if (tConn != null) { if (myDevice != null) { myDevice.setConnectionInfo(tConn); } else { updateThisDeviceAndNotify(); } } } //TODO only notif on changed device void postDeviceStatsUpdate() { for (DeviceCard c : devices) { DeviceStats s = controller.getDeviceStats(c.getDeviceID()); if (s != null) { c.setDeviceStats(s); } } } void onSystemInfoUpdate() { if (myDevice != null) { myDevice.setSystemInfo(controller.getSystemInfo()); } else { updateThisDeviceAndNotify(); } } void onFolderModelUpdate(Object o) { if (SessionController.ChangeEvent.NONE == o) { updateFoldersAndNotify(); } else { FolderSummary.Data data = (FolderSummary.Data) o; FolderCard fc = getFolderCard(data.folder); if (fc != null) { fc.setModel(data.summary); } else { updateFoldersAndNotify(); } } } void onFolderStateChange(Object o) { if (SessionController.ChangeEvent.NONE == o) { updateFolders(); } else { StateChanged.Data data = (StateChanged.Data) o; FolderCard fc = getFolderCard(data.folder); if (fc != null) { fc.setState(data.to); } else { updateFoldersAndNotify(); } } } void onFolderErrors(FolderErrors.Data data) { FolderCard fc = getFolderCard(data.folder); if (fc != null) { fc.setErrors(data.errors); } else { updateFoldersAndNotify(); } } void postFolderStatsUpdate() { for (FolderCard c : folders) { c.setStats(controller.getFolderStats(c.getId())); } } void onFolderScanProgress(FolderScanProgress.Data d) { FolderCard c = getFolderCard(d.folder); if (c != null) { c.setScanProgress(d); } } public void showSavingDialog() { dialogPresenter.showDialog(context -> { ProgressDialog mProgressDialog = new ProgressDialog(context); mProgressDialog.setMessage(context.getResources().getString(R.string.saving_config_dots)); return mProgressDialog; }); } public void dismissSavingDialog() { dialogPresenter.dismissDialog(); } void showRestartingDialog() { dialogPresenter.showDialog(context -> { ProgressDialog mProgressDialog = new ProgressDialog(context); mProgressDialog.setMessage(context.getResources().getString(R.string.syncthing_is_restarting)); return mProgressDialog; }); } void dismissRestartingDialog() { dialogPresenter.dismissDialog(); } public void showError(String title, String msg) { dialogPresenter.showDialog(context -> new AlertDialog.Builder(context) .setTitle(title) .setMessage(msg) .setPositiveButton(android.R.string.ok, null) .create()); } public void showError(int res, String msg) { dialogPresenter.showDialog(context -> new AlertDialog.Builder(context) .setTitle(res) .setMessage(msg) .setPositiveButton(android.R.string.ok, null) .create()); } public void dismissError() { dialogPresenter.dismissDialog(); } public void showSuccessMsg() { if (hasView()) { Toast.makeText(getView().getContext(), R.string.config_saved, Toast.LENGTH_SHORT).show(); } } public String getMyDeviceId() { return controller.getMyID(); } public void retryConnection() { if (!controller.isRunning()) { controller.init(); if (hasView()) { getView().showLoading(); } } } void overrideChanges(String id) { controller.overrideChanges(id); } void scanFolder(String id) { controller.scanFolder(id); } void openLoginScreen() { activityResultsController.startActivityForResult( new Intent(appContext, ManageActivity.class) .putExtra(ManageActivity.EXTRA_CREDENTIALS, (Parcelable) credentials), ActivityRequestCodes.LOGIN_ACTIVITY, null ); } void openAddDeviceScreen() { openEditDeviceScreen(null); } void openEditDeviceScreen(String deviceId) { openIntent(getEditIntent(EditDeviceFragment.NAME, EditDeviceFragment.makeArgs(credentials, deviceId))); } void openAddFolderScreen() { openEditFolderScreen(null, null); } void openEditFolderScreen(String folderId) { openEditFolderScreen(folderId, null); } void openEditFolderScreen(String folderId, String deviceId) { openIntent(getEditIntent(EditFolderFragment.NAME, EditFolderFragment.makeArgs(credentials, folderId, deviceId))); } public void openEditIgnoresScreen(String folderId) { openIntent(getEditIntent(EditIgnoresFragment.NAME, EditIgnoresFragment.makeArgs(credentials, folderId))); } void openSettingsScreen() { openIntent(getEditIntent(SettingsFragment.NAME, SettingsFragment.makeArgs(credentials))); } private Intent getEditIntent(String name, Bundle args) { Intent i = new Intent(appContext, ManageActivity.class) .putExtra(ManageActivity.EXTRA_FRAGMENT, name) .putExtra(ManageActivity.EXTRA_ARGS, args); return i; } private void openIntent(Intent i) { activityResultsController.startActivityForResult(i, 2, null); } protected void showIdDialog() { if (hasView()) { MortarScope myScope = MortarScope.getScope(getView().getContext()); final Context childContext = myScope.createContext(getView().getContext()); final String id = controller.getMyID(); AlertDialog.Builder b = new AlertDialog.Builder(childContext) .setTitle(R.string.device_id) .setView(R.layout.dialog_show_id) .setNeutralButton(R.string.close, null); if (SyncthingUtils.isClipBoardSupported(childContext)) { b.setPositiveButton(R.string.copy, (d, w) -> { SyncthingUtils.copyToClipboard(childContext, childContext.getString(R.string.device_id), id); }); b.setNegativeButton(R.string.share, (d, w) -> { SyncthingUtils.shareDeviceId(childContext, id); }); } else { b.setPositiveButton(R.string.share, (d, w) -> { SyncthingUtils.shareDeviceId(childContext, id); }); } dialogPresenter.showDialog(context -> b.create()); } } public void shutdownSyncthing() { dialogPresenter.showDialog(context -> new AlertDialog.Builder(context) .setTitle(R.string.shutdown) .setMessage(R.string.are_you_sure_you_want_to_shutdown_syncthing) .setPositiveButton(R.string.shutdown, (dialog, which) -> controller.shutdown()) .setNegativeButton(android.R.string.cancel, null) .show()); } public void restartSyncthing() { controller.restart(); } @Override public IdenticonGenerator identiconGenerator() { return identiconGenerator; } ActionBarConfig getToolbarConfig() { ActionBarConfig.Builder bob = ActionBarConfig.builder() .setTitle(credentials.alias) ; if (controller.isOnline()) { bob.setMenuConfig(new SessionMenuHandler(this)); } return bob.build(); } }