diff --git a/src/KeePassLib2Android/KeePassLib2Android.csproj b/src/KeePassLib2Android/KeePassLib2Android.csproj index 9f86c555..2c82809f 100644 --- a/src/KeePassLib2Android/KeePassLib2Android.csproj +++ b/src/KeePassLib2Android/KeePassLib2Android.csproj @@ -20,7 +20,7 @@ full False bin\Debug - DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM + DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;INCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM prompt 4 False diff --git a/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs b/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs index d840b096..cb08da30 100644 --- a/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs +++ b/src/Kp2aBusinessLogic/Io/AndroidContentStorage.cs @@ -143,6 +143,18 @@ namespace keepass2android.Io { throw new NotImplementedException(); } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + //on pre-Kitkat devices, content:// is always temporary: + return false; + } + + public bool IsReadOnly(IOConnectionInfo ioc) + { + //on pre-Kitkat devices, we can't write content:// files + return true; + } } class AndroidContentWriteTransaction : IWriteTransaction diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs index df44549e..05a75a94 100644 --- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Net; using System.Net.Security; +using System.Security; using Android.Content; using Android.OS; using Java.Security.Cert; @@ -290,5 +291,67 @@ namespace keepass2android.Io res.Path += filename; return res; } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnlyBecauseKitkatRestrictions(IOConnectionInfo ioc) + { + if (IsLocalFileFlaggedReadOnly(ioc)) + return false; //it's not read-only because of the restrictions introduced in kitkat + try + { + //test if we can open + //http://www.doubleencore.com/2014/03/android-external-storage/#comment-1294469517 + using (var writer = new Java.IO.FileOutputStream(ioc.Path, true)) + { + writer.Close(); + return false; //we can write + } + } + catch (Java.IO.IOException) + { + //seems like we can't write to that location even though it's not read-only + return true; + } + + } + + public bool IsReadOnly(IOConnectionInfo ioc) + { + if (ioc.IsLocalFile()) + { + if (IsLocalFileFlaggedReadOnly(ioc)) + return true; + if (IsReadOnlyBecauseKitkatRestrictions(ioc)) + return true; + + return false; + } + //for remote files assume they can be written: (think positive! :-) ) + return false; + } + + private bool IsLocalFileFlaggedReadOnly(IOConnectionInfo ioc) + { + try + { + return new FileInfo(ioc.Path).IsReadOnly; + } + catch (SecurityException) + { + return true; + } + catch (UnauthorizedAccessException) + { + return true; + } + catch (Exception) + { + return false; + } + } } } \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs index a1c48b8a..50b2edd6 100644 --- a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -542,6 +542,19 @@ namespace keepass2android.Io } + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + //even though the cache would be permanent, it's not a good idea to cache a temporary file, so return false in that case: + return _cachedStorage.IsPermanentLocation(ioc); + } + + public bool IsReadOnly(IOConnectionInfo ioc) + { + //even though the cache can always be written, the changes made in the cache could not be transferred to the cached file + //so we better treat the cache as read-only as well. + return _cachedStorage.IsReadOnly(ioc); + } + private void StoreFilePath(IOConnectionInfo folderPath, string filename, IOConnectionInfo res) { File.WriteAllText(CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath", res.Path); diff --git a/src/Kp2aBusinessLogic/Io/IFileStorage.cs b/src/Kp2aBusinessLogic/Io/IFileStorage.cs index 31a68ae3..a731f3c5 100644 --- a/src/Kp2aBusinessLogic/Io/IFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/IFileStorage.cs @@ -149,6 +149,17 @@ namespace keepass2android.Io /// /// The method may throw FileNotFoundException or not in case the file doesn't exist. IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename); + + /// + /// returns true if it can be expected that this location will be available permanently (in contrast to a cache copy or temporary URI permissions in Android) + /// + /// Does not require to exist forever! + bool IsPermanentLocation(IOConnectionInfo ioc); + + /// + /// Should return true if the file cannot be written. + /// + bool IsReadOnly(IOConnectionInfo ioc); } public interface IWriteTransaction: IDisposable diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index 6c7ad4b7..616cb62b 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -20,7 +20,7 @@ full false bin\Debug\ - TRACE;DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM + TRACE;DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;INCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM prompt 4 diff --git a/src/Kp2aUnitTests/Kp2aUnitTests.csproj b/src/Kp2aUnitTests/Kp2aUnitTests.csproj index c4ff09f9..f93d034a 100644 --- a/src/Kp2aUnitTests/Kp2aUnitTests.csproj +++ b/src/Kp2aUnitTests/Kp2aUnitTests.csproj @@ -67,6 +67,7 @@ + diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs index 62f0cc66..62098646 100644 --- a/src/Kp2aUnitTests/MainActivity.cs +++ b/src/Kp2aUnitTests/MainActivity.cs @@ -20,7 +20,10 @@ namespace Kp2aUnitTests // Run all tests from this assembly //runner.AddTests(Assembly.GetExecutingAssembly()); //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdb1WithKeyfileByDirectCall")); - runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdb1WithKeyfileOnly")); + //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdb1WithKeyfileOnly")); + + + runner.AddTests(new List { typeof(TestBuiltInFileStorage) }); //runner.AddTests(new List { typeof(TestSynchronizeCachedDatabase)}); //runner.AddTests(typeof(TestLoadDb).GetMethod("LoadErrorWithCertificateTrustFailure")); //runner.AddTests(typeof(TestLoadDb).GetMethod("LoadWithAcceptedCertificateTrustFailure")); diff --git a/src/Kp2aUnitTests/TestFileStorage.cs b/src/Kp2aUnitTests/TestFileStorage.cs index ef67480b..4629e684 100644 --- a/src/Kp2aUnitTests/TestFileStorage.cs +++ b/src/Kp2aUnitTests/TestFileStorage.cs @@ -183,5 +183,15 @@ namespace Kp2aUnitTests { throw new NotImplementedException(); } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnly(IOConnectionInfo ioc) + { + return false; + } } } \ No newline at end of file diff --git a/src/java/KP2AKdbLibrary/src/com/keepassdroid/database/load/ImporterV3.java b/src/java/KP2AKdbLibrary/src/com/keepassdroid/database/load/ImporterV3.java index bc319b7d..45361d4a 100644 --- a/src/java/KP2AKdbLibrary/src/com/keepassdroid/database/load/ImporterV3.java +++ b/src/java/KP2AKdbLibrary/src/com/keepassdroid/database/load/ImporterV3.java @@ -167,7 +167,7 @@ public class ImporterV3 { hdr.loadFromFile(filebuf, 0 ); if( (hdr.signature1 != PwDbHeader.PWM_DBSIG_1) || (hdr.signature2 != PwDbHeaderV3.DBSIG_2) ) { - throw new InvalidDBSignatureException(); + throw new InvalidDBSignatureException("Invalid database signature!"); } if( !hdr.matchesVersion() ) { @@ -230,7 +230,7 @@ public class ImporterV3 { } catch (IllegalBlockSizeException e1) { throw new IOException("Invalid block size"); } catch (BadPaddingException e1) { - throw new InvalidPasswordException(); + throw new InvalidPasswordException("Invalid key!"); } // Copy decrypted data for testing @@ -251,7 +251,7 @@ public class ImporterV3 { if( ! Arrays.equals(hash, hdr.contentsHash) ) { Log.w("KeePassDroid","Database file did not decrypt correctly. (checksum code is broken)"); - throw new InvalidPasswordException(); + throw new InvalidPasswordException("Invalid key!"); } // Import all groups diff --git a/src/keepass2android/Resources/Resource.designer.cs b/src/keepass2android/Resources/Resource.designer.cs index 98fcc299..97ab81ac 100644 --- a/src/keepass2android/Resources/Resource.designer.cs +++ b/src/keepass2android/Resources/Resource.designer.cs @@ -3296,6 +3296,9 @@ namespace keepass2android // aapt resource value: 0x7f070204 public const int BinaryDirectory_title = 2131165700; + // aapt resource value: 0x7f0702c5 + public const int CancelReadOnly = 2131165893; + // aapt resource value: 0x7f07026a public const int CannotMoveGroupHere = 2131165802; @@ -3305,56 +3308,56 @@ namespace keepass2android // aapt resource value: 0x7f0702bb public const int CertificateWarning = 2131165883; + // aapt resource value: 0x7f0702d8 + public const int ChangeLog = 2131165912; + + // aapt resource value: 0x7f0702d7 + public const int ChangeLog_0_7 = 2131165911; + + // aapt resource value: 0x7f0702d5 + public const int ChangeLog_0_8 = 2131165909; + + // aapt resource value: 0x7f0702d4 + public const int ChangeLog_0_8_1 = 2131165908; + + // aapt resource value: 0x7f0702d3 + public const int ChangeLog_0_8_2 = 2131165907; + + // aapt resource value: 0x7f0702d2 + public const int ChangeLog_0_8_3 = 2131165906; + + // aapt resource value: 0x7f0702d1 + public const int ChangeLog_0_8_4 = 2131165905; + // aapt resource value: 0x7f0702d0 - public const int ChangeLog = 2131165904; + public const int ChangeLog_0_8_5 = 2131165904; // aapt resource value: 0x7f0702cf - public const int ChangeLog_0_7 = 2131165903; - - // aapt resource value: 0x7f0702cd - public const int ChangeLog_0_8 = 2131165901; - - // aapt resource value: 0x7f0702cc - public const int ChangeLog_0_8_1 = 2131165900; - - // aapt resource value: 0x7f0702cb - public const int ChangeLog_0_8_2 = 2131165899; - - // aapt resource value: 0x7f0702ca - public const int ChangeLog_0_8_3 = 2131165898; - - // aapt resource value: 0x7f0702c9 - public const int ChangeLog_0_8_4 = 2131165897; - - // aapt resource value: 0x7f0702c8 - public const int ChangeLog_0_8_5 = 2131165896; - - // aapt resource value: 0x7f0702c7 - public const int ChangeLog_0_8_6 = 2131165895; - - // aapt resource value: 0x7f0702c6 - public const int ChangeLog_0_9 = 2131165894; - - // aapt resource value: 0x7f0702c5 - public const int ChangeLog_0_9_1 = 2131165893; - - // aapt resource value: 0x7f0702c4 - public const int ChangeLog_0_9_2 = 2131165892; - - // aapt resource value: 0x7f0702c3 - public const int ChangeLog_0_9_3 = 2131165891; - - // aapt resource value: 0x7f0702c2 - public const int ChangeLog_0_9_3_r5 = 2131165890; - - // aapt resource value: 0x7f0702c1 - public const int ChangeLog_0_9_4 = 2131165889; + public const int ChangeLog_0_8_6 = 2131165903; // aapt resource value: 0x7f0702ce - public const int ChangeLog_keptDonate = 2131165902; + public const int ChangeLog_0_9 = 2131165902; - // aapt resource value: 0x7f0702bf - public const int ChangeLog_title = 2131165887; + // aapt resource value: 0x7f0702cd + public const int ChangeLog_0_9_1 = 2131165901; + + // aapt resource value: 0x7f0702cc + public const int ChangeLog_0_9_2 = 2131165900; + + // aapt resource value: 0x7f0702cb + public const int ChangeLog_0_9_3 = 2131165899; + + // aapt resource value: 0x7f0702ca + public const int ChangeLog_0_9_3_r5 = 2131165898; + + // aapt resource value: 0x7f0702c9 + public const int ChangeLog_0_9_4 = 2131165897; + + // aapt resource value: 0x7f0702d6 + public const int ChangeLog_keptDonate = 2131165910; + + // aapt resource value: 0x7f0702c7 + public const int ChangeLog_title = 2131165895; // aapt resource value: 0x7f070112 public const int CheckForFileChangesOnSave_key = 2131165458; @@ -3380,9 +3383,21 @@ namespace keepass2android // aapt resource value: 0x7f070226 public const int ClearOfflineCache_title = 2131165734; + // aapt resource value: 0x7f0702c4 + public const int ClickOkToSelectLocation = 2131165892; + + // aapt resource value: 0x7f0702c2 + public const int CopyFileRequired = 2131165890; + + // aapt resource value: 0x7f0702c3 + public const int CopyFileRequiredForEditing = 2131165891; + // aapt resource value: 0x7f070117 public const int CopyToClipboardNotification_key = 2131165463; + // aapt resource value: 0x7f0702c6 + public const int CopyingFile = 2131165894; + // aapt resource value: 0x7f07025d public const int CouldNotLoadFromRemote = 2131165789; @@ -3437,6 +3452,15 @@ namespace keepass2android // aapt resource value: 0x7f070104 public const int FileHandling_prefs_key = 2131165444; + // aapt resource value: 0x7f0702c0 + public const int FileIsReadOnly = 2131165888; + + // aapt resource value: 0x7f0702c1 + public const int FileIsReadOnlyOnKitkat = 2131165889; + + // aapt resource value: 0x7f0702bf + public const int FileIsTemporarilyAvailable = 2131165887; + // aapt resource value: 0x7f07017a public const int FileNotFound = 2131165562; @@ -3524,8 +3548,8 @@ namespace keepass2android // aapt resource value: 0x7f070234 public const int PreloadDatabaseEnabled_title = 2131165748; - // aapt resource value: 0x7f0702c0 - public const int PreviewWarning = 2131165888; + // aapt resource value: 0x7f0702c8 + public const int PreviewWarning = 2131165896; // aapt resource value: 0x7f070105 public const int QuickUnlockDefaultEnabled_key = 2131165445; @@ -4169,11 +4193,11 @@ namespace keepass2android // aapt resource value: 0x7f070145 public const int brackets = 2131165509; - // aapt resource value: 0x7f0702d3 - public const int browser_intall_text = 2131165907; + // aapt resource value: 0x7f0702db + public const int browser_intall_text = 2131165915; - // aapt resource value: 0x7f0702d4 - public const int building_search_idx = 2131165908; + // aapt resource value: 0x7f0702dc + public const int building_search_idx = 2131165916; // aapt resource value: 0x7f070285 public const int button_change_location = 2131165829; @@ -4259,14 +4283,14 @@ namespace keepass2android // aapt resource value: 0x7f0700ec public const int db_key = 2131165420; - // aapt resource value: 0x7f0702d5 - public const int decrypting_db = 2131165909; + // aapt resource value: 0x7f0702dd + public const int decrypting_db = 2131165917; - // aapt resource value: 0x7f0702d6 - public const int decrypting_entry = 2131165910; + // aapt resource value: 0x7f0702de + public const int decrypting_entry = 2131165918; - // aapt resource value: 0x7f0702d7 - public const int default_checkbox = 2131165911; + // aapt resource value: 0x7f0702df + public const int default_checkbox = 2131165919; // aapt resource value: 0x7f0700de public const int default_file_path = 2131165406; @@ -4289,8 +4313,8 @@ namespace keepass2android // aapt resource value: 0x7f0700f3 public const int design_key = 2131165427; - // aapt resource value: 0x7f0702d1 - public const int design_title = 2131165905; + // aapt resource value: 0x7f0702d9 + public const int design_title = 2131165913; // aapt resource value: 0x7f070152 public const int digits = 2131165522; @@ -4352,8 +4376,8 @@ namespace keepass2android // aapt resource value: 0x7f070156 public const int entry_accessed = 2131165526; - // aapt resource value: 0x7f0702d8 - public const int entry_and_or = 2131165912; + // aapt resource value: 0x7f0702e0 + public const int entry_and_or = 2131165920; // aapt resource value: 0x7f070168 public const int entry_binaries = 2131165544; @@ -4409,8 +4433,8 @@ namespace keepass2android // aapt resource value: 0x7f07028d public const int error_adding_keyfile = 2131165837; - // aapt resource value: 0x7f0702d9 - public const int error_arc4 = 2131165913; + // aapt resource value: 0x7f0702e1 + public const int error_arc4 = 2131165921; // aapt resource value: 0x7f070169 public const int error_can_not_handle_uri = 2131165545; @@ -4424,8 +4448,8 @@ namespace keepass2android // aapt resource value: 0x7f07016c public const int error_database_exists = 2131165548; - // aapt resource value: 0x7f0702d2 - public const int error_database_settings = 2131165906; + // aapt resource value: 0x7f0702da + public const int error_database_settings = 2131165914; // aapt resource value: 0x7f07016d public const int error_database_settinoverrgs = 2131165549; @@ -4454,8 +4478,8 @@ namespace keepass2android // aapt resource value: 0x7f070174 public const int error_nopass = 2131165556; - // aapt resource value: 0x7f0702da - public const int error_out_of_memory = 2131165914; + // aapt resource value: 0x7f0702e2 + public const int error_out_of_memory = 2131165922; // aapt resource value: 0x7f070175 public const int error_pass_gen_type = 2131165557; @@ -4466,8 +4490,8 @@ namespace keepass2android // aapt resource value: 0x7f070177 public const int error_rounds_not_number = 2131165559; - // aapt resource value: 0x7f0702db - public const int error_rounds_too_large = 2131165915; + // aapt resource value: 0x7f0702e3 + public const int error_rounds_too_large = 2131165923; // aapt resource value: 0x7f07020f public const int error_string_key = 2131165711; @@ -4655,11 +4679,11 @@ namespace keepass2android // aapt resource value: 0x7f0701d6 public const int insert_element_here = 2131165654; - // aapt resource value: 0x7f0702dc - public const int install_from_market = 2131165916; + // aapt resource value: 0x7f0702e4 + public const int install_from_market = 2131165924; - // aapt resource value: 0x7f0702dd - public const int install_from_website = 2131165917; + // aapt resource value: 0x7f0702e5 + public const int install_from_website = 2131165925; // aapt resource value: 0x7f07018c public const int invalid_algorithm = 2131165580; @@ -4874,8 +4898,8 @@ namespace keepass2android // aapt resource value: 0x7f0701a6 public const int menu_hide_password = 2131165606; - // aapt resource value: 0x7f0702de - public const int menu_homepage = 2131165918; + // aapt resource value: 0x7f0702e6 + public const int menu_homepage = 2131165926; // aapt resource value: 0x7f0701a7 public const int menu_lock = 2131165607; diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 6abd3d65..5b3f891e 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -475,6 +475,17 @@ Sorry! Keepass2Android was killed by the Android OS! For security reasons, Keepass2Android did not persist your selected credentials on disk, so you need to re-open your database. Note: This should happen only very rarely. If it does, please drop me a message at crocoapps@gmail.com. + + The file is only temporarily available for Keepass2Android. + The file you selected is read-only. + The file you selected is read-only for Keepass2Android due to restrictions on Android 4.4+. + To use it, you must copy it to another location. + To edit it, you must copy the file to another location. + Click OK to select a location where the file should be copied. + Cancel, open read-only. + + Copying file... + Change log Please note! This is a preview release and might come with some flaws! If you experience *anything* unexpected, please let me know (on Codeplex or by email). diff --git a/src/keepass2android/SelectStorageLocationActivity.cs b/src/keepass2android/SelectStorageLocationActivity.cs index 691fa4de..12a8b96b 100644 --- a/src/keepass2android/SelectStorageLocationActivity.cs +++ b/src/keepass2android/SelectStorageLocationActivity.cs @@ -9,6 +9,7 @@ using Android.OS; using Android.Runtime; using Android.Views; using Android.Widget; +using Group.Pals.Android.Lib.UI.Filechooser.Utils.UI; using KeePassLib.Serialization; using keepass2android.Io; using Environment = Android.OS.Environment; @@ -16,11 +17,24 @@ using Environment = Android.OS.Environment; namespace keepass2android { [Activity(Label = "")] - public class SelectStorageLocationActivity : Activity + public class SelectStorageLocationActivity : Activity, IDialogInterfaceOnDismissListener { private ActivityDesign _design; private bool _isRecreated; - private const int RequestCodeFileStorageSelection = 983713; + private IOConnectionInfo _selectedIoc; + private const string BundleKeySelectedIoc = "BundleKeySelectedIoc"; + private const int RequestCodeFileStorageSelectionForPrimarySelect = 983713; + private const int RequestCodeFileStorageSelectionForCopyToWritableLocation = 983714; + private const int RequestCodeFileFileBrowseForWritableLocation = 983715; + + public enum WritableRequirements + { + ReadOnly = 0, + WriteDesired = 1, + WriteDemanded = 2 + } + + public const string ExtraKeyWritableRequirements = "EXTRA_KEY_WRITABLE_REQUIREMENTS"; public SelectStorageLocationActivity() { @@ -36,12 +50,12 @@ namespace keepass2android Kp2aLog.Log("SelectStorageLocationActivity.OnCreate"); - IsForSave = Intent.GetBooleanExtra(FileStorageSetupDefs.ExtraIsForSave, false); - if (IsForSave) + + if (IsStorageSelectionForSave) { throw new Exception("save is not yet implemented. In StartSelectFile, no handler for onCreate is passed."); } - + bool allowThirdPartyGet = Intent.GetBooleanExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, false); bool allowThirdPartySend = Intent.GetBooleanExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false); if (bundle == null) @@ -49,6 +63,9 @@ namespace keepass2android else { State = (Bundle)bundle.Clone(); + var selectedIocString = bundle.GetString(BundleKeySelectedIoc, null); + if (selectedIocString != null) + _selectedIoc = IOConnectionInfo.UnserializeFromString(selectedIocString); _isRecreated = true; } @@ -57,7 +74,7 @@ namespace keepass2android Intent intent = new Intent(this, typeof(FileStorageSelectionActivity)); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, allowThirdPartyGet); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, allowThirdPartySend); - StartActivityForResult(intent, RequestCodeFileStorageSelection); + StartActivityForResult(intent, RequestCodeFileStorageSelectionForPrimarySelect); } @@ -65,7 +82,10 @@ namespace keepass2android protected Bundle State { get; set; } - protected bool IsForSave { get; set; } + protected bool IsStorageSelectionForSave + { + get { return Intent.GetBooleanExtra(FileStorageSetupDefs.ExtraIsForSave, false); } + } protected override void OnSaveInstanceState(Bundle outState) @@ -73,6 +93,8 @@ namespace keepass2android base.OnSaveInstanceState(outState); outState.PutAll(State); + if (_selectedIoc != null) + outState.PutString(BundleKeySelectedIoc, IOConnectionInfo.SerializeToString(_selectedIoc)); } protected override void OnResume() @@ -84,8 +106,14 @@ namespace keepass2android protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) { base.OnActivityResult(requestCode, resultCode, data); - if (requestCode == RequestCodeFileStorageSelection) + if ((requestCode == RequestCodeFileStorageSelectionForPrimarySelect) || ((requestCode == RequestCodeFileStorageSelectionForCopyToWritableLocation))) { + int browseRequestCode = Intents.RequestCodeFileBrowseForOpen; + if (requestCode == RequestCodeFileStorageSelectionForCopyToWritableLocation) + { + browseRequestCode = RequestCodeFileFileBrowseForWritableLocation; + } + if (resultCode == KeePass.ExitFileStorageSelectionOk) { @@ -97,45 +125,55 @@ namespace keepass2android } else { + bool isForSave = (requestCode == RequestCodeFileStorageSelectionForPrimarySelect) ? + IsStorageSelectionForSave : true; + App.Kp2a.GetFileStorage(protocolId).StartSelectFile(new FileStorageSetupInitiatorActivity(this, OnActivityResult, defaultPath => - { + { if (defaultPath.StartsWith("sftp://")) - Util.ShowSftpDialog(this, OnReceivedSftpData, ReturnCancel); + Util.ShowSftpDialog(this, filename => OnReceivedSftpData(filename, browseRequestCode, isForSave), ReturnCancel); else - Util.ShowFilenameDialog(this, OnOpenButton, null, ReturnCancel, false, defaultPath, GetString(Resource.String.enter_filename_details_url), - Intents.RequestCodeFileBrowseForOpen); + //todo oncreate nur wenn for save? + Util.ShowFilenameDialog(this, filename => OnOpenButton(filename, browseRequestCode), + filename => OnOpenButton(filename, browseRequestCode), + ReturnCancel, false, defaultPath, GetString(Resource.String.enter_filename_details_url), + browseRequestCode); } - ), false, 0, protocolId); + ), isForSave, browseRequestCode, protocolId); } } else { - if (resultCode == (Result)FileStorageResults.FileChooserPrepared) - { - IOConnectionInfo ioc = new IOConnectionInfo(); - PasswordActivity.SetIoConnectionFromIntent(ioc, data); -#if !EXCLUDE_FILECHOOSER - StartFileChooser(ioc.Path); -#else - ReturnIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi.kdbx" }); -#endif - return; - } - if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE"))) - { - Toast.MakeText(this, data.GetStringExtra("EXTRA_ERROR_MESSAGE"), ToastLength.Long).Show(); - } ReturnCancel(); } } - - if (requestCode == Intents.RequestCodeFileBrowseForOpen) + + if ((requestCode == Intents.RequestCodeFileBrowseForOpen) || (requestCode == RequestCodeFileFileBrowseForWritableLocation)) { + if (resultCode == (Result)FileStorageResults.FileChooserPrepared) + { + IOConnectionInfo ioc = new IOConnectionInfo(); + PasswordActivity.SetIoConnectionFromIntent(ioc, data); +#if !EXCLUDE_FILECHOOSER + bool isForSave = (requestCode == RequestCodeFileFileBrowseForWritableLocation) ? + true : IsStorageSelectionForSave ; + + StartFileChooser(ioc.Path, requestCode, isForSave); +#else + IocSelected(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi.kdbx" }, requestCode); +#endif + return; + } + if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE"))) + { + Toast.MakeText(this, data.GetStringExtra("EXTRA_ERROR_MESSAGE"), ToastLength.Long).Show(); + } + if (resultCode == Result.Ok) { string filename = Util.IntentToFilename(data, this); @@ -152,13 +190,13 @@ namespace keepass2android Path = filename }; - ReturnIoc(ioc); + IocSelected(ioc, requestCode); } else { if (data.Data.Scheme == "content") { - ReturnIoc(IOConnectionInfo.FromPath(data.DataString)); + IocSelected(IOConnectionInfo.FromPath(data.DataString), requestCode); } else @@ -189,28 +227,154 @@ namespace keepass2android Finish(); } - private void ReturnIoc(IOConnectionInfo ioc) + private void IocSelected(IOConnectionInfo ioc, int requestCode) + { + if (requestCode == RequestCodeFileFileBrowseForWritableLocation) + { + IocForCopySelected(ioc); + } + else if (requestCode == Intents.RequestCodeFileBrowseForOpen) + { + PrimaryIocSelected(ioc); + } + else + { +#if DEBUG + throw new Exception("invalid request code!"); +#endif + } + + + + } + + private void IocForCopySelected(IOConnectionInfo targetIoc) + { + new keepass2android.Utils.SimpleLoadingDialog(this, GetString(Resource.String.CopyingFile), false, + () => + { + IOConnectionInfo sourceIoc = _selectedIoc; + + try + { + CopyFile(targetIoc, sourceIoc); + } + catch (Exception e) + { + return () => + { + Toast.MakeText(this, App.Kp2a.GetResourceString(UiStringKey.ErrorOcurred) + " " + e.Message, ToastLength.Long).Show(); + ReturnCancel(); + }; + } + + + return () => {ReturnOk(targetIoc); }; + } + ).Execute(new Object[] {}); + } + + private static void CopyFile(IOConnectionInfo targetIoc, IOConnectionInfo sourceIoc) + { + IFileStorage sourceStorage = App.Kp2a.GetFileStorage(sourceIoc); + IFileStorage targetStorage = App.Kp2a.GetFileStorage(targetIoc); + + using ( + var writeTransaction = targetStorage.OpenWriteTransaction(targetIoc, + App.Kp2a.GetBooleanPreference( + PreferenceKey.UseFileTransactions))) + { + using (var writeStream = writeTransaction.OpenFile()) + { + sourceStorage.OpenFileForRead(sourceIoc).CopyTo(writeStream); + } + writeTransaction.CommitWrite(); + } + } + + private void PrimaryIocSelected(IOConnectionInfo ioc) + { + if (!App.Kp2a.GetFileStorage(ioc).IsPermanentLocation(ioc)) + { + new AlertDialog.Builder(this) + .SetPositiveButton(Android.Resource.String.Ok, (sender, args) => { MoveToWritableLocation(ioc); }) + .SetMessage(Resources.GetString(Resource.String.FileIsTemporarilyAvailable) + " " + + Resources.GetString(Resource.String.CopyFileRequired) + " " + + Resources.GetString(Resource.String.ClickOkToSelectLocation)) + .SetCancelable(false) + .SetNegativeButton(Android.Resource.String.Cancel, (sender, args) => { ReturnCancel(); }) + //.SetOnDismissListener(this) + .Create() + .Show(); + return; + } + var filestorage = App.Kp2a.GetFileStorage(ioc); + + if ((RequestedWritableRequirements != WritableRequirements.ReadOnly) && (filestorage.IsReadOnly(ioc))) + { + string readOnlyExplanation = Resources.GetString(Resource.String.FileIsReadOnly); + BuiltInFileStorage builtInFileStorage = filestorage as BuiltInFileStorage; + if (builtInFileStorage != null) + { + if (builtInFileStorage.IsReadOnlyBecauseKitkatRestrictions(ioc)) + readOnlyExplanation = Resources.GetString(Resource.String.FileIsReadOnlyOnKitkat); + } + new AlertDialog.Builder(this) + .SetPositiveButton(Android.Resource.String.Ok, (sender, args) => { MoveToWritableLocation(ioc); }) + .SetCancelable(false) + .SetNegativeButton(Android.Resource.String.Cancel, (sender, args) => { ReturnCancel(); }) + //.SetOnDismissListener(this) + .SetMessage(readOnlyExplanation + " " + + (RequestedWritableRequirements == WritableRequirements.WriteDemanded ? + Resources.GetString(Resource.String.CopyFileRequired) + : Resources.GetString(Resource.String.CopyFileRequiredForEditing)) + + " " + + Resources.GetString(Resource.String.ClickOkToSelectLocation)) + .Create() + .Show(); + return; + } + ReturnOk(ioc); + } + + private void ReturnOk(IOConnectionInfo ioc) { Intent intent = new Intent(); PasswordActivity.PutIoConnectionToIntent(ioc, intent); SetResult(Result.Ok, intent); Finish(); + } + + private WritableRequirements RequestedWritableRequirements + { + get { return (WritableRequirements) Intent.GetIntExtra(ExtraKeyWritableRequirements, (int)WritableRequirements.ReadOnly); } + } + + private void MoveToWritableLocation(IOConnectionInfo ioc) + { + _selectedIoc = ioc; + + Intent intent = new Intent(this, typeof(FileStorageSelectionActivity)); + intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, false); + intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false); + + StartActivityForResult(intent, RequestCodeFileStorageSelectionForCopyToWritableLocation); } - private bool OnReceivedSftpData(string filename) + private bool OnReceivedSftpData(string filename, int requestCode, bool isForSave) { IOConnectionInfo ioc = new IOConnectionInfo { Path = filename }; #if !EXCLUDE_FILECHOOSER - StartFileChooser(ioc.Path); + StartFileChooser(ioc.Path, requestCode, isForSave); #else - ReturnIoc(ioc); + IocSelected(ioc, requestCode); #endif return true; } #if !EXCLUDE_FILECHOOSER - private void StartFileChooser(string defaultPath) + private void StartFileChooser(string defaultPath, int requestCode, bool forSave) { Kp2aLog.Log("FSA: defaultPath="+defaultPath); string fileProviderAuthority = FileChooserFileProvider.TheAuthority; @@ -221,24 +385,35 @@ namespace keepass2android Intent i = Keepass2android.Kp2afilechooser.Kp2aFileChooserBridge.GetLaunchFileChooserIntent(this, fileProviderAuthority, defaultPath); - StartActivityForResult(i, Intents.RequestCodeFileBrowseForOpen); + + if (forSave) + { + i.PutExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.save_dialog", true); + i.PutExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.default_file_ext", "kdbx"); + } + StartActivityForResult(i, requestCode); } #endif - private bool OnOpenButton(String fileName) + private bool OnOpenButton(String fileName, int requestCode) { + IOConnectionInfo ioc = new IOConnectionInfo { Path = fileName }; - ReturnIoc(ioc); + IocSelected(ioc, requestCode); return true; } + public void OnDismiss(IDialogInterface dialog) + { +// ReturnCancel(); + } } diff --git a/src/keepass2android/Utils/LoadingDialog.cs b/src/keepass2android/Utils/LoadingDialog.cs index 459c6d4c..274557b0 100644 --- a/src/keepass2android/Utils/LoadingDialog.cs +++ b/src/keepass2android/Utils/LoadingDialog.cs @@ -9,13 +9,13 @@ using Object = Java.Lang.Object; namespace keepass2android.Utils { - public class LoadingDialog : AsyncTask + public class LoadingDialog : AsyncTask { private readonly Context _context; private readonly string _message; private readonly bool _cancelable; - readonly Func _doInBackground; - readonly Action _onPostExecute; + private readonly Func _doInBackground; + private readonly Action _onPostExecute; private ProgressDialog mDialog; /** @@ -29,13 +29,14 @@ namespace keepass2android.Utils private Exception mLastException; - - public LoadingDialog(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + + public LoadingDialog(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) { } - public LoadingDialog(Context context, string message, bool cancelable, Func doInBackground, - Action onPostExecute) + public LoadingDialog(Context context, string message, bool cancelable, Func doInBackground, + Action onPostExecute) { _context = context; _message = message; @@ -58,7 +59,8 @@ namespace keepass2android.Utils } } - public LoadingDialog(Context context, bool cancelable, Func doInBackground, Action onPostExecute) + public LoadingDialog(Context context, bool cancelable, Func doInBackground, + Action onPostExecute) { _message = context.GetString(Resource.String.loading); _context = context; @@ -89,32 +91,41 @@ namespace keepass2android.Utils } } - , mDelayTime); + , mDelayTime); } - - + + /** * If you override this method, you must call {@code super.onCancelled()} at * beginning of the method. */ - protected override void OnCancelled() { + + protected override void OnCancelled() + { DoFinish(); base.OnCancelled(); - }// onCancelled() + } - private void DoFinish() { +// onCancelled() + + private void DoFinish() + { mFinished = true; - try { + try + { /* * Sometime the activity has been finished before we dismiss this * dialog, it will raise error. */ mDialog.Dismiss(); - } catch (Exception e) + } + catch (Exception e) { Kp2aLog.Log(e.ToString()); } - }// doFinish() + } + +// doFinish() /** @@ -124,18 +135,26 @@ namespace keepass2android.Utils * @param t * {@link Throwable} */ - protected void SetLastException(Exception e) { + + protected void SetLastException(Exception e) + { mLastException = e; - }// setLastException() + } + +// setLastException() /** * Gets last exception. * * @return {@link Throwable} */ - protected Exception GetLastException() { + + protected Exception GetLastException() + { return mLastException; - }// getLastException() + } + +// getLastException() protected override Object DoInBackground(params Object[] @params) @@ -151,14 +170,47 @@ namespace keepass2android.Utils protected override void OnPostExecute(Object result) { DoFinish(); - + if (_onPostExecute != null) _onPostExecute(result); } - - - + + + } + + public class SimpleLoadingDialog : LoadingDialog + { + private class BackgroundResult : Object + { + private readonly Action _onPostExec; + + public BackgroundResult(Action onPostExec) + { + _onPostExec = onPostExec; + } + + public Action OnPostExec + { + get { return _onPostExec; } + } + } + + public SimpleLoadingDialog(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { + } + + public SimpleLoadingDialog(Context ctx, string message, bool cancelable, Func doInBackgroundReturnOnPostExec) + : base(ctx, message, cancelable, input => + { return new BackgroundResult(doInBackgroundReturnOnPostExec()); } + , res => { ((BackgroundResult) res).OnPostExec(); }) + { + + } + + } + } \ No newline at end of file diff --git a/src/keepass2android/Utils/Util.cs b/src/keepass2android/Utils/Util.cs index 0c4e5caa..81b5d7b5 100644 --- a/src/keepass2android/Utils/Util.cs +++ b/src/keepass2android/Utils/Util.cs @@ -267,10 +267,29 @@ namespace keepass2android } } + + class CancelListener: Java.Lang.Object, IDialogInterfaceOnCancelListener + { + private readonly Action _onCancel; + + public CancelListener(Action onCancel) + { + _onCancel = onCancel; + } + + public void OnCancel(IDialogInterface dialog) + { + _onCancel(); + } + } + public static void ShowFilenameDialog(Activity activity, FileSelectedHandler onOpen, FileSelectedHandler onCreate, Action onCancel, bool showBrowseButton, string defaultFilename, string detailsText, int requestCodeBrowse) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.SetView(activity.LayoutInflater.Inflate(Resource.Layout.file_selection_filename, null)); + + if (onCancel != null) + builder.SetOnCancelListener(new CancelListener(onCancel)); Dialog dialog = builder.Create(); dialog.Show(); @@ -308,11 +327,13 @@ namespace keepass2android cancelButton.Click += delegate { dialog.Dismiss(); - + if (onCancel != null) + onCancel(); }; - if (onCancel != null) - dialog.SetOnDismissListener(new DismissListener(onCancel)); + + + ImageButton browseButton = (ImageButton) dialog.FindViewById(Resource.Id.browse_button); @@ -324,7 +345,7 @@ namespace keepass2android { string filename = ((EditText) dialog.FindViewById(Resource.Id.file_filename)).Text; - Util.ShowBrowseDialog(activity, requestCodeBrowse, onCreate != null); + ShowBrowseDialog(activity, requestCodeBrowse, onCreate != null); }; diff --git a/src/keepass2android/fileselect/FileSelectActivity.cs b/src/keepass2android/fileselect/FileSelectActivity.cs index 70c586c0..3e2afdf6 100644 --- a/src/keepass2android/fileselect/FileSelectActivity.cs +++ b/src/keepass2android/fileselect/FileSelectActivity.cs @@ -133,6 +133,7 @@ namespace keepass2android Intent intent = new Intent(this, typeof(SelectStorageLocationActivity)); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppGet, true); intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, false); + intent.PutExtra(SelectStorageLocationActivity.ExtraKeyWritableRequirements, (int) SelectStorageLocationActivity.WritableRequirements.WriteDesired); intent.PutExtra(FileStorageSetupDefs.ExtraIsForSave, false); StartActivityForResult(intent, RequestCodeSelectIoc); diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 8beec5ce..874127f0 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -30,7 +30,7 @@ full false bin\Debug - DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;EXCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM + DEBUG;EXCLUDE_TWOFISH;EXCLUDE_KEYBOARD;INCLUDE_FILECHOOSER;EXCLUDE_JAVAFILESTORAGE;INCLUDE_KEYTRANSFORM prompt 4 False