diff --git a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java index 15c6d458..bd55e387 100644 --- a/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java +++ b/platform-independent/java/algorithm/src/main/java/com/lyndir/masterpassword/MPMasterKey.java @@ -44,7 +44,8 @@ public class MPMasterKey { /** * @param masterPassword The characters of the user's master password. - * Note: this method destroys the contents of the array. + * + * @apiNote This method destroys the contents of the {@code masterPassword} array. */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") public MPMasterKey(final String fullName, final char[] masterPassword) { diff --git a/platform-independent/java/gui/build.gradle b/platform-independent/java/gui/build.gradle index 8bfb918a..04c2134e 100644 --- a/platform-independent/java/gui/build.gradle +++ b/platform-independent/java/gui/build.gradle @@ -5,7 +5,7 @@ plugins { } description = 'Master Password GUI' -mainClassName = 'com.lyndir.masterpassword.gui.Main' +mainClassName = 'com.lyndir.masterpassword.gui.MasterPassword' dependencies { implementation group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.7-p2' diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java new file mode 100644 index 00000000..3b5b3d3a --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/GUI.java @@ -0,0 +1,26 @@ +package com.lyndir.masterpassword.gui; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import com.lyndir.masterpassword.gui.util.Platform; +import com.lyndir.masterpassword.gui.util.Res; +import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; + + +/** + * @author lhunath, 2018-07-28 + */ +public class GUI { + + private static final Logger logger = Logger.get( GUI.class ); + + private final MasterPasswordFrame frame = new MasterPasswordFrame(); + + public GUI() { + Platform.get().installAppForegroundHandler( this::open ); + Platform.get().installAppReopenHandler( this::open ); + } + + public void open() { + Res.ui( () -> frame.setVisible( true ) ); + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java similarity index 71% rename from platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java rename to platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java index 9fa1fbd9..aced49f7 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/Main.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/MasterPassword.java @@ -23,10 +23,14 @@ import static com.lyndir.lhunath.opal.system.util.StringUtils.*; import com.google.common.base.Charsets; import com.google.common.io.ByteSource; import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.masterpassword.gui.platform.BaseGUI; +import com.lyndir.lhunath.opal.system.util.ObjectUtils; +import com.lyndir.masterpassword.model.MPUser; import java.io.IOException; import java.io.InputStream; import java.net.*; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArraySet; +import javax.annotation.Nullable; import javax.swing.*; @@ -35,33 +39,42 @@ import javax.swing.*; * * @author mbillemo */ -public final class Main { +public final class MasterPassword { @SuppressWarnings("UnusedDeclaration") - private static final Logger logger = Logger.get( Main.class ); + private static final Logger logger = Logger.get( MasterPassword.class ); - public static void main(final String... args) { -// Thread.setDefaultUncaughtExceptionHandler( -// (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); + private static final MasterPassword instance = new MasterPassword(); - // Try and set the system look & feel, if available. - try { - UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); - } - catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { - } + private final Collection listeners = new CopyOnWriteArraySet<>(); - // Check online to see if this version has been superseded. - if (Config.get().checkForUpdates()) - checkUpdate(); + @Nullable + private MPUser activeUser; - // Create a platform-specific GUI and open it. - BaseGUI.createPlatformGUI().open(); + public static MasterPassword get() { + return instance; + } + + public boolean addListener(final Listener listener) { + return listeners.add( listener ); + } + + public boolean removeListener(final Listener listener) { + return listeners.remove( listener ); + } + + public void activateUser(final MPUser user) { + if (ObjectUtils.equals( activeUser, user )) + return; + + activeUser = user; + for (final Listener listener : listeners) + listener.onUserSelected( activeUser ); } private static void checkUpdate() { try { - String implementationVersion = Main.class.getPackage().getImplementationVersion(); + String implementationVersion = MasterPassword.class.getPackage().getImplementationVersion(); String latestVersion = new ByteSource() { @Override public InputStream openStream() @@ -90,4 +103,27 @@ public final class Main { logger.wrn( e, "Couldn't check for version update." ); } } + + public static void main(final String... args) { + // Thread.setDefaultUncaughtExceptionHandler( + // (t, e) -> logger.bug( e, "Uncaught: %s", e.getLocalizedMessage() ) ); + + // Try and set the system look & feel, if available. + try { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException ignored) { + } + + // Check online to see if this version has been superseded. + if (Config.get().checkForUpdates()) + checkUpdate(); + + // Create a platform-specific GUI and open it. + new GUI().open(); + } + + public interface Listener { + void onUserSelected(@Nullable MPUser user); + } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java deleted file mode 100644 index 4a5731bf..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/AppleGUI.java +++ /dev/null @@ -1,62 +0,0 @@ -//============================================================================== -// This file is part of Master Password. -// Copyright (c) 2011-2017, Maarten Billemont. -// -// Master Password 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. -// -// Master Password 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 can find a copy of the GNU General Public License in the -// LICENSE file. Alternatively, see . -//============================================================================== - -package com.lyndir.masterpassword.gui.platform; - -import com.apple.eawt.*; -import com.google.common.base.Preconditions; -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import javax.swing.*; - - -/** - * @author lhunath, 2014-06-10 - */ -public class AppleGUI extends BaseGUI { - - static Application application = Preconditions.checkNotNull( - Application.getApplication(), "Not an Apple Java application." ); - - public AppleGUI() { - application.addAppEventListener( new AppEventHandler() ); - } - - @Override - protected MasterPasswordFrame createFrame() { - MasterPasswordFrame frame = super.createFrame(); - frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - return frame; - } - - private class AppEventHandler implements AppForegroundListener, AppReOpenedListener { - - @Override - public void appMovedToBackground(final AppEvent.AppForegroundEvent arg0) { - } - - @Override - public void appRaisedToForeground(final AppEvent.AppForegroundEvent arg0) { - open(); - } - - @Override - public void appReOpened(final AppEvent.AppReOpenedEvent arg0) { - open(); - } - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java deleted file mode 100644 index f0750534..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/BaseGUI.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.lyndir.masterpassword.gui.platform; - -import com.lyndir.lhunath.opal.system.logging.Logger; -import com.lyndir.lhunath.opal.system.util.TypeUtils; -import com.lyndir.masterpassword.gui.util.Res; -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import java.lang.reflect.InvocationTargetException; -import java.util.Optional; -import javax.annotation.Nullable; - - -/** - * @author lhunath, 2018-07-28 - */ -public class BaseGUI { - - private static final Logger logger = Logger.get( BaseGUI.class ); - - private final MasterPasswordFrame frame = createFrame(); - - public static BaseGUI createPlatformGUI() { - BaseGUI jdk9GUI = construct( "com.lyndir.masterpassword.gui.platform.JDK9GUI" ); - if (jdk9GUI != null) - return jdk9GUI; - - BaseGUI appleGUI = construct( "com.lyndir.masterpassword.gui.platform.AppleGUI" ); - if (appleGUI != null) - return appleGUI; - - // Use platform-independent GUI. - return new BaseGUI(); - } - - @Nullable - private static BaseGUI construct(final String typeName) { - try { - // AppleGUI adds support for macOS features. - Optional> gui = TypeUtils.loadClass( typeName ); - if (gui.isPresent()) - return gui.get().getConstructor().newInstance(); - } - catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) { - } - catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { - throw logger.bug( e ); - } - - return null; - } - - protected MasterPasswordFrame createFrame() { - return new MasterPasswordFrame(); - } - - public void open() { - Res.ui( () -> frame.setVisible( true ) ); - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java deleted file mode 100644 index 7970f32b..00000000 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/JDK9GUI.java +++ /dev/null @@ -1,60 +0,0 @@ -//============================================================================== -// This file is part of Master Password. -// Copyright (c) 2011-2017, Maarten Billemont. -// -// Master Password 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. -// -// Master Password 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 can find a copy of the GNU General Public License in the -// LICENSE file. Alternatively, see . -//============================================================================== - -package com.lyndir.masterpassword.gui.platform; - -import com.lyndir.masterpassword.gui.view.MasterPasswordFrame; -import java.awt.*; -import java.awt.desktop.*; -import javax.swing.*; - - -/** - * @author lhunath, 2014-06-10 - */ -@SuppressWarnings("Since15") -public class JDK9GUI extends BaseGUI { - - public JDK9GUI() { - Desktop.getDesktop().addAppEventListener( new AppEventHandler() ); - } - - @Override - protected MasterPasswordFrame createFrame() { - MasterPasswordFrame frame = super.createFrame(); - frame.setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); - return frame; - } - - private class AppEventHandler implements AppForegroundListener, AppReopenedListener { - - @Override - public void appRaisedToForeground(final AppForegroundEvent e) { - open(); - } - - @Override - public void appMovedToBackground(final AppForegroundEvent e) { - } - - @Override - public void appReopened(final AppReopenedEvent e) { - open(); - } - } -} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java index 229bd22b..99107ae9 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Components.java @@ -21,6 +21,7 @@ package com.lyndir.masterpassword.gui.util; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.function.Consumer; @@ -110,6 +111,29 @@ public abstract class Components { return (option < 0)? JOptionPane.CLOSED_OPTION: option; } + @Nullable + public static File showLoadDialog(@Nullable final Component owner, final String title) { + return showFileDialog( owner, title, FileDialog.LOAD, null ); + } + + @Nullable + public static File showSaveDialog(@Nullable final Component owner, final String title, final String fileName) { + return showFileDialog( owner, title, FileDialog.SAVE, fileName ); + } + + @Nullable + private static File showFileDialog(@Nullable final Component owner, final String title, + final int mode, @Nullable final String fileName) { + FileDialog fileDialog = new FileDialog( JOptionPane.getFrameForComponent( owner ), title, mode ); + fileDialog.setFile( fileName ); + fileDialog.setLocationRelativeTo( owner ); + fileDialog.setLocationByPlatform( true ); + fileDialog.setVisible( true ); + + File[] selectedFiles = fileDialog.getFiles(); + return ((selectedFiles != null) && (selectedFiles.length > 0))? selectedFiles[0]: null; + } + public static JDialog showDialog(@Nullable final Component owner, @Nullable final String title, final Container content) { JDialog dialog = new JDialog( (owner != null)? SwingUtilities.windowForComponent( owner ): null, title, Dialog.ModalityType.DOCUMENT_MODAL ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java new file mode 100644 index 00000000..5a7b8029 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Platform.java @@ -0,0 +1,52 @@ +package com.lyndir.masterpassword.gui.util; + +import com.lyndir.lhunath.opal.system.logging.Logger; +import com.lyndir.lhunath.opal.system.util.TypeUtils; +import com.lyndir.masterpassword.gui.util.platform.BasePlatform; +import com.lyndir.masterpassword.gui.util.platform.IPlatform; +import java.lang.reflect.InvocationTargetException; +import java.util.Optional; +import javax.annotation.Nullable; + + +/** + * @author lhunath, 2018-07-29 + */ +public final class Platform { + + private static final Logger logger = Logger.get( Platform.class ); + private static final IPlatform activePlatform; + + static { + IPlatform tryPlatform; + if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.JDK9Platform" ))) + activePlatform = tryPlatform; + + else if (null != (tryPlatform = construct( "com.lyndir.masterpassword.gui.util.platform.ApplePlatform" ))) + activePlatform = tryPlatform; + + else + activePlatform = new BasePlatform(); + } + + @Nullable + private static T construct(final String typeName) { + try { + // AppleGUI adds support for macOS features. + Optional> gui = TypeUtils.loadClass( typeName ); + if (gui.isPresent()) + return gui.get().getConstructor().newInstance(); + } + catch (@SuppressWarnings("ErrorNotRethrown") final LinkageError ignored) { + } + catch (final IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { + throw logger.bug( e ); + } + + return null; + } + + public static IPlatform get() { + return activePlatform; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java index e4bba494..c29896e1 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/Res.java @@ -136,6 +136,14 @@ public abstract class Res { return icon( "media/icon_reset.png" ); } + public Icon import_() { + return icon( "media/icon_import.png" ); + } + + public Icon export() { + return icon( "media/icon_export.png" ); + } + public Icon settings() { return icon( "media/icon_settings.png" ); } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java new file mode 100644 index 00000000..961bcc89 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/ApplePlatform.java @@ -0,0 +1,49 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import com.apple.eawt.*; +import com.apple.eio.FileManager; +import com.google.common.base.Preconditions; +import com.google.common.base.Throwables; +import java.io.File; +import java.io.FileNotFoundException; + + +/** + * @author lhunath, 2018-07-29 + */ +public class ApplePlatform implements IPlatform { + + static Application application = Preconditions.checkNotNull( + Application.getApplication(), "Not an Apple Java application." ); + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + application.addAppEventListener( new AppForegroundListener() { + @Override + public void appMovedToBackground(final AppEvent.AppForegroundEvent e) { + } + + @Override + public void appRaisedToForeground(final AppEvent.AppForegroundEvent e) { + handler.run(); + } + } ); + return true; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + application.addAppEventListener( (AppReOpenedListener) e -> handler.run() ); + return true; + } + + @Override + public boolean show(final File file) { + try { + return FileManager.revealInFinder( file ); + } + catch (final FileNotFoundException ignored) { + return false; + } + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java new file mode 100644 index 00000000..b6b41ea3 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/BasePlatform.java @@ -0,0 +1,25 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +public class BasePlatform implements IPlatform { + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + return false; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + return false; + } + + @Override + public boolean show(final File file) { + return false; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java new file mode 100644 index 00000000..a6f3d48e --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/IPlatform.java @@ -0,0 +1,16 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +public interface IPlatform { + + boolean installAppForegroundHandler(Runnable handler); + + boolean installAppReopenHandler(Runnable handler); + + boolean show(File file); +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java new file mode 100644 index 00000000..1bc94782 --- /dev/null +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/JDK9Platform.java @@ -0,0 +1,43 @@ +package com.lyndir.masterpassword.gui.util.platform; + +import java.awt.*; +import java.awt.desktop.*; +import java.io.File; + + +/** + * @author lhunath, 2018-07-29 + */ +@SuppressWarnings("Since15") +public class JDK9Platform implements IPlatform { + + @Override + public boolean installAppForegroundHandler(final Runnable handler) { + Desktop.getDesktop().addAppEventListener( new AppForegroundListener() { + @Override + public void appRaisedToForeground(final AppForegroundEvent e) { + handler.run(); + } + + @Override + public void appMovedToBackground(final AppForegroundEvent e) { + } + } ); + return true; + } + + @Override + public boolean installAppReopenHandler(final Runnable handler) { + Desktop.getDesktop().addAppEventListener( (AppReopenedListener) e -> handler.run() ); + return true; + } + + @Override + public boolean show(final File file) { + if (!file.exists()) + return false; + + Desktop.getDesktop().browseFileDirectory( file ); + return true; + } +} diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java similarity index 95% rename from platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java rename to platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java index 0168e33e..f4786cc2 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/platform/package-info.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/util/platform/package-info.java @@ -20,6 +20,6 @@ * @author lhunath, 2018-04-26 */ @ParametersAreNonnullByDefault -package com.lyndir.masterpassword.gui.platform; +package com.lyndir.masterpassword.gui.util.platform; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java index 8aa0778c..c42a102c 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/FilesPanel.java @@ -3,6 +3,7 @@ package com.lyndir.masterpassword.gui.view; import static com.lyndir.masterpassword.util.Utilities.*; import com.google.common.collect.ImmutableSortedSet; +import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.util.*; import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUser; @@ -18,15 +19,13 @@ import javax.swing.*; * @author lhunath, 2018-07-14 */ @SuppressWarnings("serial") -public class FilesPanel extends JPanel implements MPFileUserManager.Listener { - - private final Collection listeners = new CopyOnWriteArraySet<>(); +public class FilesPanel extends JPanel implements MPFileUserManager.Listener, MasterPassword.Listener { private final JButton avatarButton = Components.button( Res.icons().avatar( 0 ), event -> setAvatar(), "Click to change the user's avatar." ); private final CollectionListModel> usersModel = - CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( this::setUser ); + CollectionListModel.>copy( MPFileUserManager.get().getFiles() ).selection( MasterPassword.get()::activateUser ); private final JComboBox> userField = Components.comboBox( usersModel, user -> ifNotNull( user, MPUser::getFullName ) ); @@ -50,10 +49,7 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener { add( userField ); MPFileUserManager.get().addListener( this ); - } - - public boolean addListener(final Listener listener) { - return listeners.add( listener ); + MasterPassword.get().addListener( this ); } private void setAvatar() { @@ -65,20 +61,14 @@ public class FilesPanel extends JPanel implements MPFileUserManager.Listener { avatarButton.setIcon( Res.icons().avatar( selectedUser.getAvatar() ) ); } - public void setUser(@Nullable final MPUser user) { - avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); - - for (final Listener listener : listeners) - listener.onUserSelected( user ); - } - @Override public void onFilesUpdated(final ImmutableSortedSet files) { usersModel.set( files ); } - public interface Listener { - - void onUserSelected(@Nullable MPUser user); + @Override + public void onUserSelected(@Nullable final MPUser user) { + usersModel.setSelectedItem( user ); + avatarButton.setIcon( Res.icons().avatar( (user == null)? 0: user.getAvatar() ) ); } } diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java index 2ac3698f..baf3a66e 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/MasterPasswordFrame.java @@ -3,10 +3,12 @@ package com.lyndir.masterpassword.gui.view; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.gui.util.Components; import com.lyndir.masterpassword.gui.util.Res; +import com.lyndir.masterpassword.model.MPUser; import com.lyndir.masterpassword.model.impl.MPFileUserManager; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; +import javax.annotation.Nullable; import javax.swing.*; import javax.swing.border.BevelBorder; @@ -40,8 +42,6 @@ public class MasterPasswordFrame extends JFrame { BorderFactory.createBevelBorder( BevelBorder.RAISED, Res.colors().controlBorder(), Res.colors().frameBg() ), Res.colors().controlBg(), BoxLayout.PAGE_AXIS, userContent ), BorderLayout.CENTER ); - filesPanel.addListener( userContent ); - addComponentListener( new ComponentHandler() ); setPreferredSize( new Dimension( 800, 560 ) ); setDefaultCloseOperation( DISPOSE_ON_CLOSE ); diff --git a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java index bc7e1307..aa064d7e 100644 --- a/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java +++ b/platform-independent/java/gui/src/main/java/com/lyndir/masterpassword/gui/view/UserContentPanel.java @@ -8,15 +8,22 @@ import com.google.common.primitives.UnsignedInteger; import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.util.ObjectUtils; import com.lyndir.masterpassword.*; +import com.lyndir.masterpassword.gui.MasterPassword; import com.lyndir.masterpassword.gui.model.MPNewSite; import com.lyndir.masterpassword.gui.util.*; +import com.lyndir.masterpassword.gui.util.Platform; import com.lyndir.masterpassword.model.*; import com.lyndir.masterpassword.model.impl.*; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.*; +import java.util.Optional; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -31,20 +38,22 @@ import javax.swing.event.DocumentListener; * @author lhunath, 2018-07-14 */ @SuppressWarnings("SerializableStoresNonSerializable") -public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPUser.Listener { +public class UserContentPanel extends JPanel implements MasterPassword.Listener, MPUser.Listener { private static final Random random = new Random(); private static final Logger logger = Logger.get( UserContentPanel.class ); private static final JButton iconButton = Components.button( Res.icons().user(), null, null ); - private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(), - "Add a new user to Master Password." ); + private final JButton addButton = Components.button( Res.icons().add(), event -> addUser(), + "Add a new user to Master Password." ); + private final JButton importButton = Components.button( Res.icons().import_(), event -> importUser(), + "Import a user from a backup file into Master Password." ); private final JPanel userToolbar = Components.panel( BoxLayout.PAGE_AXIS ); private final JPanel siteToolbar = Components.panel( BoxLayout.PAGE_AXIS ); @Nullable - private MPUser activeUser; + private MPUser showingUser; private ContentMode contentMode; public UserContentPanel() { @@ -53,7 +62,9 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); setBorder( Components.marginBorder() ); - setUser( null ); + showUser( null ); + + MasterPassword.get().addListener( this ); } protected JComponent getUserToolbar() { @@ -66,52 +77,52 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU @Override public void onUserSelected(@Nullable final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserUpdated(final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserAuthenticated(final MPUser user) { - setUser( user ); + showUser( user ); } @Override public void onUserInvalidated(final MPUser user) { - setUser( user ); + showUser( user ); } - private void setUser(@Nullable final MPUser user) { + private void showUser(@Nullable final MPUser user) { Res.ui( () -> { - if (activeUser != null) - activeUser.removeListener( this ); + if (showingUser != null) + showingUser.removeListener( this ); ContentMode newContentMode = ContentMode.getContentMode( user ); - if ((newContentMode != contentMode) || !ObjectUtils.equals( activeUser, user )) { + if ((newContentMode != contentMode) || !ObjectUtils.equals( showingUser, user )) { userToolbar.removeAll(); siteToolbar.removeAll(); removeAll(); - activeUser = user; + showingUser = user; switch (contentMode = newContentMode) { case NO_USER: add( new NoUserPanel() ); break; case AUTHENTICATE: - add( new AuthenticateUserPanel( Preconditions.checkNotNull( activeUser ) ) ); + add( new AuthenticateUserPanel( Preconditions.checkNotNull( showingUser ) ) ); break; case AUTHENTICATED: - add( new AuthenticatedUserPanel( Preconditions.checkNotNull( activeUser ) ) ); + add( new AuthenticatedUserPanel( Preconditions.checkNotNull( showingUser ) ) ); break; } revalidate(); transferFocus(); } - if (activeUser != null) - activeUser.addListener( this ); + if (showingUser != null) + showingUser.addListener( this ); } ); } @@ -122,7 +133,70 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU if (fullName == null) return; - setUser( MPFileUserManager.get().add( fullName.toString() ) ); + MasterPassword.get().activateUser( MPFileUserManager.get().add( fullName.toString() ) ); + } + + private void importUser() { + File importFile = Components.showLoadDialog( this, "Import User File" ); + if (importFile == null) + return; + + try { + MPFileUser importUser = MPFileUser.load( importFile ); + if (importUser == null) { + JOptionPane.showMessageDialog( + this, "Not a Master Password file.", + "Import Failed", JOptionPane.ERROR_MESSAGE ); + return; + } + + JPasswordField passwordField = Components.passwordField(); + if (JOptionPane.OK_OPTION == Components.showDialog( this, "Import User", new JOptionPane( Components.panel( + BoxLayout.PAGE_AXIS, + Components.label( strf( "Enter the master password to import %s:", + importUser.getFullName() ) ), + Components.strut(), + passwordField ), JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION ) { + @Override + public void selectInitialValue() { + passwordField.requestFocusInWindow(); + } + } )) { + try { + importUser.authenticate( passwordField.getPassword() ); + Optional existingUser = MPFileUserManager.get().getFiles().stream().filter( + user -> user.getFullName().equalsIgnoreCase( importUser.getFullName() ) ).findFirst(); + if (existingUser.isPresent() && (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog( + this, + strf( "Importing user %s from this file will replace the existing user with the imported one.
" + + "Are you sure?

" + + "Existing user last modified: %s
Imported user last modified: %s
", + importUser.getFullName(), + Res.format( existingUser.get().getLastUsed() ), + Res.format( importUser.getLastUsed() ) ) ))) + return; + + MasterPassword.get().activateUser( MPFileUserManager.get().add( importUser ) ); + } + catch (final MPIncorrectMasterPasswordException | MPAlgorithmException e) { + JOptionPane.showMessageDialog( + this, e.getLocalizedMessage(), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } + } + } + catch (final IOException e) { + logger.err( e, "While reading user import file." ); + JOptionPane.showMessageDialog( + this, strf( "Couldn't read import file:
%s
.", e.getLocalizedMessage() ), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } + catch (MPMarshalException e) { + logger.err( e, "While parsing user import file." ); + JOptionPane.showMessageDialog( + this, strf( "Couldn't parse import file:
%s
.", e.getLocalizedMessage() ), + "Import Failed", JOptionPane.ERROR_MESSAGE ); + } } private enum ContentMode { @@ -147,6 +221,7 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU setLayout( new BoxLayout( this, BoxLayout.PAGE_AXIS ) ); userToolbar.add( addButton ); + userToolbar.add( importButton ); add( Box.createGlue() ); add( Components.heading( "Select a user to proceed." ) ); @@ -160,6 +235,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU @Nonnull private final MPUser user; + private final JButton exportButton = Components.button( Res.icons().export(), event -> exportUser(), + "Export this user to a backup file." ); private final JButton deleteButton = Components.button( Res.icons().delete(), event -> deleteUser(), "Delete this user from Master Password." ); private final JButton resetButton = Components.button( Res.icons().reset(), event -> resetUser(), @@ -177,6 +254,8 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU this.user = user; userToolbar.add( addButton ); + userToolbar.add( importButton ); + userToolbar.add( exportButton ); userToolbar.add( deleteButton ); userToolbar.add( resetButton ); @@ -197,6 +276,27 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU add( Box.createGlue() ); } + private void exportUser() { + MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; + if (fileUser == null) + return; + + File exportFile = Components.showSaveDialog( this, "Export User File", fileUser.getFile().getName() ); + if (exportFile == null) + return; + + try { + Platform.get().show( + Files.copy( fileUser.getFile().toPath(), exportFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES ).toFile() ); + } + catch (final IOException e) { + JOptionPane.showMessageDialog( + this, e.getLocalizedMessage(), + "Export Failed", JOptionPane.ERROR_MESSAGE ); + } + } + private void deleteUser() { MPFileUser fileUser = (user instanceof MPFileUser)? (MPFileUser) user: null; if (fileUser == null) @@ -372,7 +472,17 @@ public class UserContentPanel extends JPanel implements FilesPanel.Listener, MPU sitesModel.registerList( sitesList ); add( Box.createGlue() ); - user.addListener( this ); + addComponentListener( new ComponentAdapter() { + @Override + public void componentShown(final ComponentEvent e) { + user.addListener( AuthenticatedUserPanel.this ); + } + + @Override + public void componentHidden(final ComponentEvent e) { + user.removeListener( AuthenticatedUserPanel.this ); + } + } ); } public void showUserPreferences() { diff --git a/platform-independent/java/gui/src/main/resources/media/icon_export.png b/platform-independent/java/gui/src/main/resources/media/icon_export.png new file mode 100644 index 00000000..d1d97fb2 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_export.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_export@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_export@2x.png new file mode 100644 index 00000000..37c9e4f6 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_export@2x.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_import.png b/platform-independent/java/gui/src/main/resources/media/icon_import.png new file mode 100644 index 00000000..5cdfd6e0 Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_import.png differ diff --git a/platform-independent/java/gui/src/main/resources/media/icon_import@2x.png b/platform-independent/java/gui/src/main/resources/media/icon_import@2x.png new file mode 100644 index 00000000..e8a2650a Binary files /dev/null and b/platform-independent/java/gui/src/main/resources/media/icon_import@2x.png differ diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java index e9be467c..f8242940 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/MPUser.java @@ -55,12 +55,11 @@ public interface MPUser> extends Comparable> { /** * Performs an authentication attempt against the keyID for this user. * - * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password. - * * @param masterPassword The password to authenticate with. - * You cannot re-use this array after passing it in, authentication will destroy its contents. * * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID. + * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given master password. + * This method destroys the contents of the {@code masterPassword} array. */ void authenticate(char[] masterPassword) throws MPIncorrectMasterPasswordException, MPAlgorithmException; @@ -68,11 +67,10 @@ public interface MPUser> extends Comparable> { /** * Performs an authentication attempt against the keyID for this user. * - * Note: If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key. - * * @param masterKey The master key to authenticate with. * * @throws MPIncorrectMasterPasswordException If authentication fails due to the given master password not matching the user's keyID. + * @apiNote If a keyID is not set, authentication will always succeed and the keyID will be set to match the given key. */ void authenticate(MPMasterKey masterKey) throws MPIncorrectMasterPasswordException, MPKeyUnavailableException, MPAlgorithmException; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java index eef907c2..6821eeaf 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPBasicUser.java @@ -37,8 +37,9 @@ import javax.annotation.Nullable; */ public abstract class MPBasicUser> extends Changeable implements MPUser { - protected final Logger logger = Logger.get( getClass() ); - private final Set listeners = new CopyOnWriteArraySet<>(); + private static final Logger logger = Logger.get( MPBasicUser.class ); + + private final Set listeners = new CopyOnWriteArraySet<>(); private int avatar; private final String fullName; @@ -65,7 +66,7 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAvatar(final int avatar) { - if (Objects.equals(this.avatar, avatar)) + if (Objects.equals( this.avatar, avatar )) return; this.avatar = avatar; @@ -86,7 +87,7 @@ public abstract class MPBasicUser> extends Changeabl @Override public void setAlgorithm(final MPAlgorithm algorithm) { - if (Objects.equals(this.algorithm, algorithm)) + if (Objects.equals( this.algorithm, algorithm )) return; this.algorithm = algorithm; diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java index acbdf2bc..f8fd88a2 100755 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUser.java @@ -18,6 +18,7 @@ package com.lyndir.masterpassword.model.impl; +import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.masterpassword.*; import com.lyndir.masterpassword.model.MPIncorrectMasterPasswordException; import com.lyndir.masterpassword.model.MPUser; @@ -36,6 +37,8 @@ import org.joda.time.ReadableInstant; @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") public class MPFileUser extends MPBasicUser { + private static final Logger logger = Logger.get( MPFileUser.class ); + @Nullable private byte[] keyID; private File path; @@ -46,13 +49,23 @@ public class MPFileUser extends MPBasicUser { private ReadableInstant lastUsed; private boolean complete; - public MPFileUser(final String fullName) { - this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm() ); + @Nullable + public static MPFileUser load(final File file) + throws IOException, MPMarshalException { + for (final MPMarshalFormat format : MPMarshalFormat.values()) + if (file.getName().endsWith( format.fileSuffix() )) + return format.unmarshaller().readUser( file ); + + return null; } - public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm) { + public MPFileUser(final String fullName, final File path) { + this( fullName, null, MPAlgorithm.Version.CURRENT.getAlgorithm(), path ); + } + + public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, final File path) { this( fullName, keyID, algorithm, 0, null, new Instant(), - MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, MPFileUserManager.get().getPath() ); + MPMarshaller.ContentMode.PROTECTED, MPMarshalFormat.DEFAULT, path ); } public MPFileUser(final String fullName, @Nullable final byte[] keyID, final MPAlgorithm algorithm, @@ -74,6 +87,10 @@ public class MPFileUser extends MPBasicUser { return (keyID == null)? null: keyID.clone(); } + public void setPath(final File path) { + this.path = path; + } + @Override public void setAlgorithm(final MPAlgorithm algorithm) { if (!algorithm.equals( getAlgorithm() ) && (keyID != null)) { @@ -99,7 +116,7 @@ public class MPFileUser extends MPBasicUser { } public void setFormat(final MPMarshalFormat format) { - if (Objects.equals(this.format, format)) + if (Objects.equals( this.format, format )) return; this.format = format; @@ -111,7 +128,7 @@ public class MPFileUser extends MPBasicUser { } public void setContentMode(final MPMarshaller.ContentMode contentMode) { - if (Objects.equals(this.contentMode, contentMode)) + if (Objects.equals( this.contentMode, contentMode )) return; this.contentMode = contentMode; @@ -123,7 +140,7 @@ public class MPFileUser extends MPBasicUser { } public void setDefaultType(final MPResultType defaultType) { - if (Objects.equals(this.defaultType, defaultType)) + if (Objects.equals( this.defaultType, defaultType )) return; this.defaultType = defaultType; @@ -169,6 +186,19 @@ public class MPFileUser extends MPBasicUser { } } + public void save() { + try { + if (isComplete()) + getFormat().marshaller().marshall( this ); + } + catch (final MPKeyUnavailableException e) { + logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); + } + catch (final IOException | MPMarshalException | MPAlgorithmException e) { + logger.err( e, "Unable to write out changes for user: %s", this ); + } + } + @Override public void reset() { keyID = null; @@ -183,16 +213,7 @@ public class MPFileUser extends MPBasicUser { @Override protected void onChanged() { - try { - if (isComplete()) - getFormat().marshaller().marshall( this ); - } - catch (final MPKeyUnavailableException e) { - logger.wrn( e, "Cannot write out changes for unauthenticated user: %s.", this ); - } - catch (final IOException | MPMarshalException | MPAlgorithmException e) { - logger.err( e, "Unable to write out changes for user: %s", this ); - } + save(); super.onChanged(); } diff --git a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java index d90eff13..b3d260dd 100644 --- a/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java +++ b/platform-independent/java/model/src/main/java/com/lyndir/masterpassword/model/impl/MPFileUserManager.java @@ -78,31 +78,40 @@ public class MPFileUserManager { } for (final File file : pathFiles) - for (final MPMarshalFormat format : MPMarshalFormat.values()) - if (file.getName().endsWith( format.fileSuffix() )) - try { - MPFileUser user = format.unmarshaller().readUser( file ); - MPFileUser previousUser = userByName.put( user.getFullName(), user ); - if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) - userByName.put( previousUser.getFullName(), previousUser ); - break; - } - catch (final IOException | MPMarshalException e) { - logger.err( e, "Couldn't read user from: %s", file ); - } + try { + MPFileUser user = MPFileUser.load( file ); + if (user != null) { + MPFileUser previousUser = userByName.put( user.getFullName(), user ); + if ((previousUser != null) && (previousUser.getFormat().ordinal() > user.getFormat().ordinal())) + userByName.put( previousUser.getFullName(), previousUser ); + } + } + catch (final IOException | MPMarshalException e) { + logger.err( e, "Couldn't read user from: %s", file ); + } fireUpdated(); } public MPFileUser add(final String fullName) { - MPFileUser user = new MPFileUser( fullName ); - userByName.put( user.getFullName(), user ); + return add( new MPFileUser( fullName, getPath() ) ); + } + + public MPFileUser add(final MPFileUser user) { + user.setPath( getPath() ); + user.save(); + + MPFileUser oldUser = userByName.put( user.getFullName(), user ); + if (oldUser != null) + oldUser.invalidate(); fireUpdated(); return user; } public void delete(final MPFileUser user) { + user.invalidate(); + // Remove deleted users. File userFile = user.getFile(); if (userFile.exists() && !userFile.delete())