/*
* Kontalk Java client
* Copyright (C) 2016 Kontalk Devteam <devteam@kontalk.org>
*
* 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 org.kontalk.view;
import javax.swing.event.DocumentEvent;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.util.EnumMap;
import java.util.List;
import java.util.Observable;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.alee.extended.filechooser.FilesSelectionListener;
import com.alee.extended.filechooser.WebFileChooserField;
import com.alee.extended.panel.GroupPanel;
import com.alee.laf.button.WebButton;
import com.alee.laf.checkbox.WebCheckBox;
import com.alee.laf.label.WebLabel;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.rootpane.WebDialog;
import com.alee.laf.separator.WebSeparator;
import com.alee.laf.text.WebPasswordField;
import com.alee.utils.swing.DocumentChangeListener;
import org.kontalk.misc.KonException;
import org.kontalk.system.AccountImporter;
import org.kontalk.util.Tr;
/**
* Wizard-like dialog for importing new key files.
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
final class ImportDialog extends WebDialog {
private static final Logger LOGGER = Logger.getLogger(ImportDialog.class.getName());
private enum ImportPage {INTRO, SETTINGS, RESULT}
private enum Direction {BACK, FORTH}
private final EnumMap<ImportPage, ImportPanel> mPanels;
private final WebButton mBackButton;
private final WebButton mNextButton;
private final WebButton mCancelButton;
private final WebButton mFinishButton;
private final View mView;
private final boolean mConnect;
private final ResultPanel mResultPanel;
private ImportPage mCurrentPage;
// exchanged between panels
private String mZipPath = "";
private char[] mPasswd = {};
ImportDialog(final View view, final boolean connect) {
mView = view;
mConnect = connect;
this.setTitle(Tr.tr("Import Wizard"));
this.setSize(420, 300);
this.setResizable(false);
this.setModal(true);
this.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_DEFAULT));
// buttons
mBackButton = new WebButton(Tr.tr("Back"));
mBackButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mResultPanel.mayAbort();
ImportDialog.this.switchPage(Direction.BACK);
}
});
mNextButton = new WebButton(Tr.tr("Next"));
mNextButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ImportDialog.this.switchPage(Direction.FORTH);
}
});
mCancelButton = new WebButton(Tr.tr("Cancel"));
mCancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mResultPanel.mayAbort();
ImportDialog.this.dispose();
}
});
mFinishButton = new WebButton(Tr.tr("Done"));
mFinishButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mPanels.get(mCurrentPage).onNext();
}
});
mFinishButton.setVisible(false);
GroupPanel buttonPanel = new GroupPanel(mBackButton, mNextButton,
mCancelButton, mFinishButton);
buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING));
this.add(buttonPanel, BorderLayout.SOUTH);
// panels
mPanels = new EnumMap<>(ImportPage.class);
mPanels.put(ImportPage.INTRO, new IntroPanel());
mPanels.put(ImportPage.SETTINGS, new SettingsPanel());
mResultPanel = new ResultPanel();
mPanels.put(ImportPage.RESULT, mResultPanel);
this.setPage(ImportPage.INTRO);
}
private void switchPage(Direction dir) {
int step = dir == Direction.BACK ? -1 : +1;
ImportPage[] pages = ImportPage.values();
ImportPage newPage = pages[mCurrentPage.ordinal() + step];
ImportPanel oldPanel = mPanels.get(mCurrentPage);
if (dir == Direction.FORTH)
oldPanel.onNext();
this.remove(oldPanel);
this.setPage(newPage);
}
private void setPage(ImportPage newPage) {
mCurrentPage = newPage;
ImportPanel newPanel = mPanels.get(mCurrentPage);
newPanel.onShow();
this.add(newPanel, BorderLayout.CENTER);
// swing is messy again
this.repaint();
}
private abstract class ImportPanel extends WebPanel {
abstract protected void onShow();
void onNext() {}
}
private class IntroPanel extends ImportPanel {
IntroPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Get Started")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
// html tag for word wrap
String text = "<html>"+
Tr.tr("Welcome to the import wizard.")+" "
+Tr.tr("To use the Kontalk desktop client you need an existing account.")+" "
+Tr.tr("Please export the personal key files from your Android device and select them on the next page.")
+"</html>";
groupPanel.add(new WebLabel(text));
this.add(groupPanel);
}
@Override
protected void onShow() {
mBackButton.setVisible(false);
mNextButton.setEnabled(true);
}
}
private class SettingsPanel extends ImportPanel {
private final WebFileChooserField mZipFileChooser;
private final WebPasswordField mPassField;
SettingsPanel() {
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Setup")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
// file chooser for key files
groupPanel.add(new WebLabel(Tr.tr("Zip archive containing personal key:")));
mZipFileChooser = createFileChooser(".kontalk-keys.zip");
mZipFileChooser.addSelectedFilesListener(new FilesSelectionListener() {
@Override
public void selectionChanged(List<File> files) {
SettingsPanel.this.checkNextButton();
}
});
groupPanel.add(mZipFileChooser);
groupPanel.add(new WebSeparator(true, true));
// text field for passphrase
groupPanel.add(new WebLabel(Tr.tr("Decryption password for personal key:")));
mPassField = new WebPasswordField(42);
mPassField.setInputPrompt(Tr.tr("Enter password…"));
mPassField.setHideInputPromptOnFocus(false);
mPassField.getDocument().addDocumentListener(new DocumentChangeListener() {
@Override
public void documentChanged(DocumentEvent e) {
SettingsPanel.this.checkNextButton();
}
});
groupPanel.add(mPassField);
WebCheckBox showPasswordBox = new WebCheckBox(Tr.tr("Show password"));
showPasswordBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
boolean selected = e.getStateChange() == ItemEvent.SELECTED;
mPassField.setEchoChar(selected ? (char)0 : '*');
}
});
groupPanel.add(showPasswordBox);
this.add(groupPanel);
}
private void checkNextButton() {
mNextButton.setEnabled(!mZipFileChooser.getSelectedFiles().isEmpty() &&
!String.valueOf(mPassField.getPassword()).isEmpty());
}
@Override
protected void onShow() {
mBackButton.setVisible(true);
mNextButton.setVisible(true);
mCancelButton.setVisible(true);
mFinishButton.setVisible(false);
this.checkNextButton();
}
@Override
protected void onNext() {
if (!mZipFileChooser.getSelectedFiles().isEmpty())
mZipPath = mZipFileChooser.getSelectedFiles().get(0).getAbsolutePath();
mPasswd = mPassField.getPassword();
}
}
private class ResultPanel extends ImportPanel implements ObserverTrait {
private final AccountImporter mImporter;
private final WebLabel mResultLabel;
private final WebLabel mErrorLabel;
private final ComponentUtils.PassPanel mPassPanel;
private boolean mWaiting = false;
ResultPanel() {
mImporter = mView.getControl().createAccountImporter();
mImporter.addObserver(this);
GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false);
groupPanel.setMargin(View.MARGIN_BIG);
groupPanel.add(new WebLabel(Tr.tr("Import results")).setBoldFont());
groupPanel.add(new WebSeparator(true, true));
mResultLabel = new WebLabel();
groupPanel.add(mResultLabel);
mErrorLabel = new WebLabel();
groupPanel.add(mErrorLabel);
mPassPanel = new ComponentUtils.PassPanel(false) {
@Override
void onValidInput() {
mFinishButton.setEnabled(true);
}
@Override
void onInvalidInput() {
mFinishButton.setEnabled(false);
}
};
groupPanel.add(mPassPanel);
this.add(groupPanel);
}
@Override
protected void onShow() {
mNextButton.setVisible(false);
mCancelButton.setVisible(true);
this.importAccount();
}
private void importAccount() {
if (mZipPath.isEmpty()) {
LOGGER.warning("no zip file path");
return;
}
mResultLabel.setText(Tr.tr("Waiting..."));
mWaiting = true;
mImporter.fromZipFile(mZipPath, mPasswd);
}
@Override
public void updateOnEDT(Observable o, Object arg) {
if (arg == null) {
this.onResult(null);
} else if (arg instanceof KonException) {
this.onResult((KonException) arg);
} else {
LOGGER.warning("unexpected argument: "+arg);
}
}
private void onResult(KonException ex) {
mWaiting = false;
String errorText = null;
if (ex != null) {
errorText = Utils.getErrorText(ex);
} else {
mCancelButton.setVisible(false);
mFinishButton.setVisible(true);
}
mPassPanel.setVisible(ex == null);
String result = ex == null ? Tr.tr("Success!") : Tr.tr("Error");
mResultLabel.setText(Tr.tr("Import process finished with:")+" "+result);
mErrorLabel.setText(errorText == null ?
"" :
"<html>"+Tr.tr("Error description:")+" \n\n"+errorText+"</html>");
}
private void mayAbort() {
if (!mWaiting)
return;
mImporter.abort();
mWaiting = false;
}
@Override
protected void onNext() {
char[] newPass = mPassPanel.getNewPassword().orElse(null);
if (newPass != null && newPass.length > 0) {
try {
mView.getControl().setAccountPassword(new char[0], newPass);
} catch (KonException ex) {
LOGGER.log(Level.WARNING, "can't set password", ex);
return;
}
}
ImportDialog.this.dispose();
if (mConnect)
mView.getControl().connect();
}
}
private static WebFileChooserField createFileChooser(String path) {
final WebFileChooserField fileChooser = new WebFileChooserField();
fileChooser.setMultiSelectionEnabled(false);
fileChooser.setShowFileShortName(false);
fileChooser.setShowRemoveButton(false);
fileChooser.getWebFileChooser().setFileFilter(new FileNameExtensionFilter(Tr.tr("Zip archive"), "zip"));
File file = new File(path);
if (file.exists()) {
fileChooser.setSelectedFile(file);
} else {
fileChooser.setBorderColor(Color.RED);
}
if (file.getParentFile() != null && file.getParentFile().exists())
fileChooser.getWebFileChooser().setCurrentDirectory(file.getParentFile());
fileChooser.addSelectedFilesListener(new FilesSelectionListener() {
@Override
public void selectionChanged(List<File> files) {
for (File file : files) {
if (file.exists()) {
fileChooser.setBorderColor(Color.BLACK);
}
}
}
});
return fileChooser;
}
}