added UI for opening a database with OTPs. Some TODOs and things not yet working!
This commit is contained in:
		| @@ -3,6 +3,7 @@ using Android.App; | ||||
| using System.IO; | ||||
| using Android.Content; | ||||
| using Android.OS; | ||||
| using KeePassLib.Keys; | ||||
| using KeePassLib.Serialization; | ||||
| using keepass2android.Io; | ||||
|  | ||||
| @@ -22,7 +23,7 @@ namespace keepass2android | ||||
| 		/// <summary> | ||||
| 		/// Loads the specified data as the currently open database, as unlocked. | ||||
| 		/// </summary> | ||||
| 		void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string s, string keyFile, ProgressDialogStatusLogger statusLogger); | ||||
| 		void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, ProgressDialogStatusLogger statusLogger); | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// Returns the current database | ||||
|   | ||||
| @@ -107,16 +107,6 @@ namespace keepass2android.Io | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public bool CompleteIoId() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public bool? FileExists() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			return UrlUtil.StripExtension( | ||||
| @@ -207,5 +197,19 @@ namespace keepass2android.Io | ||||
| 				parent += "/"; | ||||
| 			return parent + newFilename; | ||||
| 		} | ||||
|  | ||||
| 		public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			return IoUtil.GetParentPath(ioc); | ||||
| 		} | ||||
|  | ||||
| 		public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) | ||||
| 		{ | ||||
| 			IOConnectionInfo res = folderPath.CloneDeep(); | ||||
| 			if (!res.Path.EndsWith("/")) | ||||
| 				res.Path += "/"; | ||||
| 			res.Path += filename; | ||||
| 			return res; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -400,16 +400,6 @@ namespace keepass2android.Io | ||||
| 			return new CachedWriteTransaction(ioc, useFileTransaction, this); | ||||
| 		} | ||||
|  | ||||
| 		public bool CompleteIoId() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public bool? FileExists() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			return UrlUtil.StripExtension( | ||||
| @@ -487,6 +477,54 @@ namespace keepass2android.Io | ||||
| 			return _cachedStorage.CreateFilePath(parent, newFilename); | ||||
| 		} | ||||
|  | ||||
| 		public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			return _cachedStorage.GetParentPath(ioc); | ||||
| 		} | ||||
|  | ||||
| 		public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) | ||||
| 		{ | ||||
| 			try | ||||
| 			{ | ||||
| 				IOConnectionInfo res = _cachedStorage.GetFilePath(folderPath, filename); | ||||
| 				//some file storage implementations require accessing the network to determine the file path (e.g. because | ||||
| 				//they might contain file ids). In this case, we need to cache the result to enable cached access to such files | ||||
| 				StoreFilePath(folderPath, filename, res); | ||||
| 				return res; | ||||
| 			} | ||||
| 			catch (Exception) | ||||
| 			{ | ||||
| 				IOConnectionInfo res; | ||||
| 				if (!TryGetCachedFilePath(folderPath, filename, out res)) throw; | ||||
| 				return res; | ||||
| 			} | ||||
| 			 | ||||
| 		} | ||||
|  | ||||
| 		private void StoreFilePath(IOConnectionInfo folderPath, string filename, IOConnectionInfo res) | ||||
| 		{ | ||||
| 			File.WriteAllText(CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath", res.Path); | ||||
| 		} | ||||
|  | ||||
| 		private IOConnectionInfo GetPseudoIoc(IOConnectionInfo folderPath, string filename) | ||||
| 		{ | ||||
| 			IOConnectionInfo res = folderPath.CloneDeep(); | ||||
| 			if (!res.Path.EndsWith("/")) | ||||
| 				res.Path += "/"; | ||||
| 			res.Path += filename; | ||||
| 			return res; | ||||
| 		} | ||||
|  | ||||
| 		private bool TryGetCachedFilePath(IOConnectionInfo folderPath, string filename, out IOConnectionInfo res) | ||||
| 		{ | ||||
| 			res = folderPath.CloneDeep(); | ||||
| 			string filePathCache = CachedFilePath(GetPseudoIoc(folderPath, filename)) + ".filepath"; | ||||
| 			if (!File.Exists(filePathCache)) | ||||
| 				return false; | ||||
| 			res.Path = File.ReadAllText(filePathCache); | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
|  | ||||
| 		public string GetBaseVersionHash(IOConnectionInfo ioc) | ||||
| 		{ | ||||
|   | ||||
| @@ -82,19 +82,6 @@ namespace keepass2android.Io | ||||
| 		/// <param name="useFileTransaction">if true, force to use file system level transaction. This might be ignored if the file storage has built in transaction support</param> | ||||
| 		IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction); | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// brings up a dialog to query credentials or something like this. | ||||
| 		/// </summary> | ||||
| 		/// <returns>true if success, false if error or cancelled by user</returns> | ||||
| 		bool CompleteIoId( /*in/out ioId*/); | ||||
|  | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// Checks whether the given file exists. | ||||
| 		/// </summary> | ||||
| 		/// <returns>true if it exists, false if not. Null if the check couldn't be performed (e.g. because no credentials available or no connection established.)</returns> | ||||
| 		bool? FileExists( /*ioId*/); | ||||
|  | ||||
| 		string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc); | ||||
| 		 | ||||
| 		/// <summary> | ||||
| @@ -135,10 +122,10 @@ namespace keepass2android.Io | ||||
| 		void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId); | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// Initiates the process for choosing a file in the given file storage. | ||||
| 		/// Initiates the process for using a file in the given file storage. | ||||
| 		/// The file storage should either call OnImmediateResult or StartFileUsageProcess | ||||
| 		/// If alwaysReturnSuccess is true, the activity should be finished with ResultCode Ok. | ||||
| 		/// This can make sense if a higher-level file storage has the file cached by still wants to  | ||||
| 		/// This can make sense if a higher-level file storage has the file cached but still wants to  | ||||
| 		/// give the cached storage the chance to initialize file access. | ||||
| 		/// </summary> | ||||
| 		void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, bool alwaysReturnSuccess); | ||||
| @@ -157,6 +144,17 @@ namespace keepass2android.Io | ||||
| 		//returns the path of a file "newFilename" in the folder "parent" | ||||
| 		//this may create the file if this is required to get a path (if a UUID is part of the file path) | ||||
| 		string CreateFilePath(string parent, string newFilename); | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// returns the parent folder of ioc | ||||
| 		/// </summary> | ||||
| 		IOConnectionInfo GetParentPath(IOConnectionInfo ioc); | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// returns the file path of the file "filename" in the folderPath. | ||||
| 		/// </summary> | ||||
| 		/// The method may throw FileNotFoundException or not in case the file doesn't exist. | ||||
| 		IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename); | ||||
| 	} | ||||
|  | ||||
| 	public interface IWriteTransaction: IDisposable | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using Java.IO; | ||||
| using KeePassLib.Serialization; | ||||
|  | ||||
| namespace keepass2android.Io | ||||
| { | ||||
| @@ -30,5 +31,20 @@ namespace keepass2android.Io | ||||
| 		} | ||||
|  | ||||
|  | ||||
| 		public static IOConnectionInfo GetParentPath(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			var iocParent = ioc.CloneDeep(); | ||||
| 			if (iocParent.Path.EndsWith("/")) | ||||
| 				iocParent.Path = iocParent.Path.Substring(0, iocParent.Path.Length - 1); | ||||
|  | ||||
| 			int slashPos = iocParent.Path.LastIndexOf("/", StringComparison.Ordinal); | ||||
| 			if (slashPos == -1) | ||||
| 				iocParent.Path = ""; | ||||
| 			else | ||||
| 			{ | ||||
| 				iocParent.Path = iocParent.Path.Substring(0, slashPos); | ||||
| 			} | ||||
| 			return iocParent; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -151,16 +151,6 @@ namespace keepass2android.Io | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public bool CompleteIoId() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public bool? FileExists() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) | ||||
| 		{ | ||||
| 			return UrlUtil.StripExtension( | ||||
|   | ||||
| @@ -82,29 +82,14 @@ namespace keepass2android | ||||
| 			 | ||||
| 		} | ||||
|  | ||||
| 		 | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// Do not call this method directly. Call App.Kp2a.LoadDatabase instead. | ||||
| 		/// </summary> | ||||
| 		public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, String password, String keyfile, ProgressDialogStatusLogger status) | ||||
| 		public void LoadData(IKp2aApp app, IOConnectionInfo iocInfo, MemoryStream databaseData, CompositeKey compositeKey, ProgressDialogStatusLogger status) | ||||
| 		{ | ||||
| 			PwDatabase pwDatabase = new PwDatabase(); | ||||
|  | ||||
| 			CompositeKey compositeKey = new CompositeKey(); | ||||
| 			compositeKey.AddUserKey(new KcpPassword(password)); | ||||
| 			if (!String.IsNullOrEmpty(keyfile)) | ||||
| 			{ | ||||
|  | ||||
| 				try | ||||
| 				{ | ||||
| 					compositeKey.AddUserKey(new KcpKeyFile(keyfile)); | ||||
| 				} catch (Exception e) | ||||
| 				{ | ||||
| 					Kp2aLog.Log(e.ToString()); | ||||
| 					throw new KeyFileException(); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			IFileStorage fileStorage = _app.GetFileStorage(iocInfo); | ||||
| 			var filename = fileStorage.GetFilenameWithoutPathAndExt(iocInfo); | ||||
| 			try | ||||
| @@ -115,7 +100,9 @@ namespace keepass2android | ||||
| 			} | ||||
| 			catch (InvalidCompositeKeyException) | ||||
| 			{ | ||||
| 				if ((password == "") && (keyfile != null)) | ||||
| 				KcpPassword passwordKey = (KcpPassword)compositeKey.GetUserKey(typeof(KcpPassword)); | ||||
| 			 | ||||
| 				if ((passwordKey != null) && (passwordKey.Password.ReadString() == "") && (compositeKey.UserKeyCount > 1)) | ||||
| 				{ | ||||
| 					//if we don't get a password, we don't know whether this means "empty password" or "no password" | ||||
| 					//retry without password: | ||||
|   | ||||
| @@ -19,6 +19,7 @@ using System; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using KeePassLib.Keys; | ||||
| using KeePassLib.Serialization; | ||||
|  | ||||
| namespace keepass2android | ||||
| @@ -26,20 +27,20 @@ namespace keepass2android | ||||
| 	public class LoadDb : RunnableOnFinish { | ||||
| 		private readonly IOConnectionInfo _ioc; | ||||
| 		private readonly Task<MemoryStream> _databaseData; | ||||
| 		private readonly String _pass; | ||||
| 		private readonly String _key; | ||||
| 		private readonly CompositeKey _compositeKey; | ||||
| 		private readonly string _keyfileOrProvider; | ||||
| 		private readonly IKp2aApp _app; | ||||
| 		private readonly bool _rememberKeyfile; | ||||
| 		 | ||||
| 		public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, String pass, String key, OnFinish finish): base(finish) | ||||
| 		public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, String keyfileOrProvider, OnFinish finish): base(finish) | ||||
| 		{ | ||||
| 			_app = app; | ||||
| 			_ioc = ioc; | ||||
| 			_databaseData = databaseData; | ||||
| 			_pass = pass; | ||||
| 			_key = key; | ||||
| 			_compositeKey = compositeKey; | ||||
| 			_keyfileOrProvider = keyfileOrProvider; | ||||
|  | ||||
|  | ||||
|              | ||||
| 			_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile);  | ||||
| 		} | ||||
| 		 | ||||
| @@ -50,8 +51,8 @@ namespace keepass2android | ||||
| 			{ | ||||
| 				StatusLogger.UpdateMessage(UiStringKey.loading_database); | ||||
| 				MemoryStream memoryStream = _databaseData == null ? null : _databaseData.Result; | ||||
| 				_app.LoadDatabase(_ioc, memoryStream, _pass, _key, StatusLogger); | ||||
| 				SaveFileData(_ioc, _key); | ||||
| 				_app.LoadDatabase(_ioc, memoryStream, _compositeKey, StatusLogger); | ||||
| 				SaveFileData(_ioc, _keyfileOrProvider); | ||||
|  | ||||
| 			} | ||||
| 			catch (KeyFileException) | ||||
| @@ -88,13 +89,13 @@ namespace keepass2android | ||||
| 			Finish(true); | ||||
| 		} | ||||
| 		 | ||||
| 		private void SaveFileData(IOConnectionInfo ioc, String key) { | ||||
| 		private void SaveFileData(IOConnectionInfo ioc, String keyfileOrProvider) { | ||||
|  | ||||
|             if (!_rememberKeyfile) | ||||
|             { | ||||
|                 key = ""; | ||||
|                 keyfileOrProvider = ""; | ||||
|             } | ||||
|             _app.StoreOpenedFileAsRecent(ioc, key); | ||||
|             _app.StoreOpenedFileAsRecent(ioc, keyfileOrProvider); | ||||
| 		} | ||||
| 		 | ||||
| 		 | ||||
|   | ||||
| @@ -85,15 +85,6 @@ namespace Kp2aUnitTests | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public bool CompleteIoId() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public bool? FileExists() | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) | ||||
| 		{ | ||||
|   | ||||
| @@ -38,7 +38,7 @@ namespace Kp2aUnitTests | ||||
| 		public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string password, string keyFile, | ||||
| 		                         ProgressDialogStatusLogger statusLogger) | ||||
| 		{ | ||||
| 			_db.LoadData(this, ioConnectionInfo, memoryStream, password, keyFile, statusLogger); | ||||
| 			_db.LoadData(this, ioConnectionInfo, memoryStream, password, statusLogger); | ||||
| 			 | ||||
| 		} | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -16,13 +16,16 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file | ||||
|   */ | ||||
|  | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using Android.App; | ||||
| using Android.Content; | ||||
| using Android.Database; | ||||
| using Android.OS; | ||||
| using Android.Runtime; | ||||
| using Android.Views; | ||||
| using Android.Widget; | ||||
| using Java.Lang; | ||||
| using Java.Net; | ||||
| using Android.Preferences; | ||||
| using Java.IO; | ||||
| @@ -31,8 +34,14 @@ using Android.Content.PM; | ||||
| using KeePassLib.Keys; | ||||
| using KeePassLib.Serialization; | ||||
| using KeePassLib.Utility; | ||||
| using OtpKeyProv; | ||||
| using keepass2android.Io; | ||||
| using keepass2android.Utils; | ||||
| using Exception = System.Exception; | ||||
| using MemoryStream = System.IO.MemoryStream; | ||||
| using Object = Java.Lang.Object; | ||||
| using Process = Android.OS.Process; | ||||
| using String = System.String; | ||||
|  | ||||
| namespace keepass2android | ||||
| { | ||||
| @@ -41,6 +50,15 @@ namespace keepass2android | ||||
| 	           Theme="@style/Base")] | ||||
|  | ||||
| 	public class PasswordActivity : LockingActivity { | ||||
|  | ||||
| 		enum KeyProviders | ||||
| 		{ | ||||
| 			//int values correspond to indices in passwordSpinner | ||||
| 			None = 0, | ||||
| 			KeyFile = 1, | ||||
| 			Otp = 2 | ||||
| 		} | ||||
|  | ||||
| 		bool _showPassword; | ||||
|  | ||||
| 		public const String KeyDefaultFilename = "defaultFileName"; | ||||
| @@ -53,14 +71,37 @@ namespace keepass2android | ||||
|  | ||||
| 		private const String ViewIntent = "android.intent.action.VIEW"; | ||||
| 		private const string ShowpasswordKey = "ShowPassword"; | ||||
| 		private const string KeyProviderIdOtp = "KP2A-OTP"; | ||||
|  | ||||
| 		private Task<MemoryStream> _loadDbTask; | ||||
| 		private IOConnectionInfo _ioConnection; | ||||
| 		private String _keyFile; | ||||
| 		private String _keyFileOrProvider; | ||||
|  | ||||
| 		internal AppTask AppTask; | ||||
| 		private bool _killOnDestroy; | ||||
| 		private string _password = ""; | ||||
| 		private const int RequestCodePrepareDbFile = 1000; | ||||
| 		private const int RequestCodePrepareOtpAuxFile = 1001; | ||||
|  | ||||
|  | ||||
| 		KeyProviders KeyProviderType | ||||
| 		{ | ||||
| 			get | ||||
| 			{ | ||||
| 				if (_keyFileOrProvider == null) | ||||
| 					return KeyProviders.None; | ||||
| 				if (_keyFileOrProvider == KeyProviderIdOtp) | ||||
| 					return KeyProviders.Otp; | ||||
| 				return KeyProviders.KeyFile; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		private bool _rememberKeyfile; | ||||
| 		ISharedPreferences _prefs; | ||||
|  | ||||
| 		private bool _starting; | ||||
| 		private OtpInfo _otpInfo; | ||||
| 		private readonly int[] _otpTextViewIds = new int[] {Resource.Id.otp1, Resource.Id.otp2, Resource.Id.otp3, Resource.Id.otp4, Resource.Id.otp5, Resource.Id.otp6}; | ||||
|  | ||||
| 		public PasswordActivity (IntPtr javaReference, JniHandleOwnership transfer) | ||||
| 			: base(javaReference, transfer) | ||||
| @@ -170,6 +211,7 @@ namespace keepass2android | ||||
| 							KcpKeyFile kcpKeyfile = (KcpKeyFile)App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(typeof(KcpKeyFile)); | ||||
|  | ||||
| 							SetEditText(Resource.Id.pass_keyfile, kcpKeyfile.Path); | ||||
| 							_keyFileOrProvider = kcpKeyfile.Path; | ||||
| 						} | ||||
| 					} | ||||
| 					App.Kp2a.LockDatabase(false); | ||||
| @@ -186,18 +228,62 @@ namespace keepass2android | ||||
| 							 | ||||
| 							EditText fn = (EditText) FindViewById(Resource.Id.pass_keyfile); | ||||
| 							fn.Text = filename; | ||||
| 							_keyFileOrProvider = filename; | ||||
| 						} | ||||
| 					} | ||||
| 					break; | ||||
| 				case (Result)FileStorageResults.FileUsagePrepared: | ||||
| 					PeformLoadDatabase(); | ||||
| 					if (requestCode == RequestCodePrepareDbFile) | ||||
| 						PerformLoadDatabase(); | ||||
| 					if (requestCode == RequestCodePrepareOtpAuxFile) | ||||
| 						LoadOtpFile(); | ||||
| 					break; | ||||
| 			} | ||||
| 			 | ||||
| 		} | ||||
|  | ||||
| 		internal AppTask AppTask; | ||||
| 		private bool _killOnDestroy; | ||||
| 		private void LoadOtpFile() | ||||
| 		{ | ||||
| 			new LoadingDialog<object, object, object>(this, true,  | ||||
| 				//doInBackground | ||||
| 				delegate | ||||
| 				{ | ||||
| 					_otpInfo = OathHotpKeyProv.LoadOtpInfo(new KeyProviderQueryContext(_ioConnection, false, false)); | ||||
| 					return null; | ||||
| 				}, | ||||
| 				//onPostExecute | ||||
| 				delegate | ||||
| 					{ | ||||
| 						if (_otpInfo == null) | ||||
| 						{ | ||||
| 							Toast.MakeText(this, GetString(Resource.String.CouldntLoadOtpAuxFile), ToastLength.Long).Show(); | ||||
| 							return; | ||||
| 						} | ||||
| 						FindViewById(Resource.Id.init_otp).Visibility = ViewStates.Gone; | ||||
| 						FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible; | ||||
| 						int c = 0; | ||||
| 					foreach (int otpId in _otpTextViewIds) | ||||
| 					{ | ||||
| 						c++; | ||||
| 						var otpTextView = FindViewById<EditText>(otpId); | ||||
| 						otpTextView.Text = ""; | ||||
| 						otpTextView.Hint = GetString(Resource.String.otp_hint, new Object[] {c}); | ||||
| 						otpTextView.SetFilters(new IInputFilter[] {new InputFilterLengthFilter((int)_otpInfo.OtpLength) }); | ||||
| 						if (c > _otpInfo.OtpsRequired) | ||||
| 						{ | ||||
| 							otpTextView.Visibility = ViewStates.Gone; | ||||
| 						} | ||||
| 						else | ||||
| 						{ | ||||
| 							otpTextView.TextChanged += (sender, args) =>  | ||||
| 							{ | ||||
| 								UpdateOkButtonState(); | ||||
| 							}; | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			).Execute(); | ||||
| 		} | ||||
|  | ||||
| 		protected override void OnCreate(Bundle savedInstanceState) | ||||
| 		{ | ||||
| @@ -245,15 +331,15 @@ namespace keepass2android | ||||
| 					return; | ||||
| 				} | ||||
| 				 | ||||
| 				_keyFile = GetKeyFile(_ioConnection.Path); | ||||
| 				_keyFileOrProvider = GetKeyFile(_ioConnection.Path); | ||||
| 				 | ||||
| 			} else | ||||
| 			{ | ||||
| 				SetIoConnectionFromIntent(_ioConnection, i); | ||||
| 				_keyFile = i.GetStringExtra(KeyKeyfile); | ||||
| 				if (string.IsNullOrEmpty(_keyFile)) | ||||
| 				_keyFileOrProvider = i.GetStringExtra(KeyKeyfile); | ||||
| 				if (string.IsNullOrEmpty(_keyFileOrProvider)) | ||||
| 				{ | ||||
| 					_keyFile = GetKeyFile(_ioConnection.Path); | ||||
| 					_keyFileOrProvider = GetKeyFile(_ioConnection.Path); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| @@ -271,6 +357,19 @@ namespace keepass2android | ||||
|  | ||||
| 			EditText passwordEdit = FindViewById<EditText>(Resource.Id.password); | ||||
|  | ||||
| 			FindViewById<EditText>(Resource.Id.pass_keyfile).TextChanged += | ||||
| 				(sender, args) => | ||||
| 					{ | ||||
| 						_keyFileOrProvider = FindViewById<EditText>(Resource.Id.pass_keyfile).Text; | ||||
| 						UpdateOkButtonState(); | ||||
| 					}; | ||||
|  | ||||
| 			FindViewById<EditText>(Resource.Id.password).TextChanged += | ||||
| 				(sender, args) => | ||||
| 				{ | ||||
| 					_password = FindViewById<EditText>(Resource.Id.password).Text; | ||||
| 					UpdateOkButtonState(); | ||||
| 				}; | ||||
|  | ||||
| 			passwordEdit.RequestFocus(); | ||||
| 			Window.SetSoftInputMode(SoftInput.StateVisible); | ||||
| @@ -279,10 +378,48 @@ namespace keepass2android | ||||
| 			confirmButton.Click += (sender, e) => | ||||
| 				{ | ||||
| 					App.Kp2a.GetFileStorage(_ioConnection) | ||||
| 					   .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, 0, false); | ||||
| 					   .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareDbFile, false); | ||||
| 				}; | ||||
|  | ||||
| 			Spinner passwordModeSpinner = FindViewById<Spinner>(Resource.Id.password_mode_spinner); | ||||
| 			if (passwordModeSpinner != null) | ||||
| 			{ | ||||
|  | ||||
| 				UpdateKeyProviderUiState(); | ||||
| 				passwordModeSpinner.SetSelection((int) KeyProviderType); | ||||
| 				passwordModeSpinner.ItemSelected += (sender, args) => | ||||
| 					{ | ||||
| 						switch (args.Position) | ||||
| 						{ | ||||
| 							case 0: | ||||
| 								_keyFileOrProvider = null; | ||||
| 								break; | ||||
| 							case 1: | ||||
| 								_keyFileOrProvider = ""; | ||||
| 								break; | ||||
| 							case 2: | ||||
| 								_keyFileOrProvider = KeyProviderIdOtp; | ||||
| 								break; | ||||
| 							default: | ||||
| 								throw new Exception("Unexpected position "+args.Position+" / " + ((ICursor)((AdapterView)sender).GetItemAtPosition(args.Position)).GetString(1)); | ||||
|  | ||||
| 						} | ||||
| 						UpdateKeyProviderUiState(); | ||||
| 					}; | ||||
| 				FindViewById(Resource.Id.init_otp).Click += (sender, args) => | ||||
| 					{ | ||||
| 						App.Kp2a.GetOtpAuxFileStorage(_ioConnection) | ||||
| 						.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareOtpAuxFile, false); | ||||
| 					}; | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				//android 2.x  | ||||
| 				//TODO test | ||||
| 			} | ||||
|  | ||||
|  | ||||
| 			UpdateOkButtonState(); | ||||
| 			 | ||||
| 			 | ||||
| 			 | ||||
| @@ -327,14 +464,96 @@ namespace keepass2android | ||||
| 			RetrieveSettings(); | ||||
| 		} | ||||
|  | ||||
| 		private void PeformLoadDatabase() | ||||
| 		private void UpdateOkButtonState() | ||||
| 		{ | ||||
| 			String pass = GetEditText(Resource.Id.password); | ||||
| 			String key = GetEditText(Resource.Id.pass_keyfile); | ||||
| 			if (pass.Length == 0 && key.Length == 0) | ||||
| 			switch (KeyProviderType) | ||||
| 			{ | ||||
| 				ErrorMessage(Resource.String.error_nopass); | ||||
| 				return; | ||||
| 				case KeyProviders.None: | ||||
| 					FindViewById(Resource.Id.pass_ok).Enabled = true; | ||||
| 					break; | ||||
| 				case KeyProviders.KeyFile: | ||||
| 					FindViewById(Resource.Id.pass_ok).Enabled = _keyFileOrProvider != "" || _password != ""; | ||||
| 					break; | ||||
| 				case KeyProviders.Otp: | ||||
| 					 | ||||
| 					bool enabled = true; | ||||
| 					if (_otpInfo == null) | ||||
| 						enabled = false; | ||||
| 					else | ||||
| 					{ | ||||
| 						int c = 0; | ||||
| 						foreach (int otpId in _otpTextViewIds) | ||||
| 						{ | ||||
| 							c++; | ||||
| 							var otpTextView = FindViewById<EditText>(otpId); | ||||
| 							if ((c <= _otpInfo.OtpsRequired) && (otpTextView.Text == "")) | ||||
| 							{ | ||||
| 								enabled = false; | ||||
| 								break; | ||||
| 							} | ||||
| 						}	 | ||||
| 					} | ||||
| 					 | ||||
| 					FindViewById(Resource.Id.pass_ok).Enabled = enabled; | ||||
| 					break; | ||||
| 				default: | ||||
| 					throw new ArgumentOutOfRangeException(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		private void UpdateKeyProviderUiState() | ||||
| 		{ | ||||
| 			FindViewById(Resource.Id.keyfileLine).Visibility = KeyProviderType == KeyProviders.KeyFile | ||||
| 				                                                   ? ViewStates.Visible | ||||
| 				                                                   : ViewStates.Gone; | ||||
| 			FindViewById(Resource.Id.otpView).Visibility = KeyProviderType == KeyProviders.Otp | ||||
| 				                                               ? ViewStates.Visible | ||||
| 				                                               : ViewStates.Gone; | ||||
| 			UpdateOkButtonState(); | ||||
| 		} | ||||
|  | ||||
| 		private void PerformLoadDatabase() | ||||
| 		{ | ||||
| 			//no need to check for validity of password because if this method is called, the Ok button was enabled (i.e. there was a valid password) | ||||
| 			CompositeKey compositeKey = new CompositeKey(); | ||||
| 			compositeKey.AddUserKey(new KcpPassword(_password)); | ||||
| 			if (KeyProviderType == KeyProviders.KeyFile) | ||||
| 			{ | ||||
|  | ||||
| 				try | ||||
| 				{ | ||||
| 					compositeKey.AddUserKey(new KcpKeyFile(_keyFileOrProvider)); | ||||
| 				} | ||||
| 				catch (Exception e) | ||||
| 				{ | ||||
| 					Kp2aLog.Log(e.ToString()); | ||||
| 					throw new KeyFileException(); | ||||
| 				} | ||||
| 			} | ||||
| 			else if (KeyProviderType == KeyProviders.Otp) | ||||
| 			{ | ||||
|  | ||||
| 				try | ||||
| 				{ | ||||
| 					List<string> lOtps = new List<string>(); | ||||
| 					foreach (int otpId in _otpTextViewIds) | ||||
| 					{ | ||||
| 						string otpText = FindViewById<EditText>(otpId).Text; | ||||
| 						if (!String.IsNullOrEmpty(otpText)) | ||||
| 							lOtps.Add(otpText); | ||||
| 					} | ||||
| 					CreateOtpSecret(lOtps); | ||||
| 				} | ||||
| 				catch (Exception) | ||||
| 				{ | ||||
| 					const string strMain = "Failed to create OTP key!"; | ||||
| 					const string strLine1 = "Make sure you've entered the correct OTPs."; | ||||
|  | ||||
| 					Toast.MakeText(this, strMain + " " + strLine1, ToastLength.Long).Show(); | ||||
|  | ||||
| 					return; | ||||
| 				} | ||||
| 				compositeKey.AddUserKey(new KcpCustomKey(OathHotpKeyProv.Name, _otpInfo.Secret, true)); | ||||
| 			} | ||||
|  | ||||
| 			CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock); | ||||
| @@ -345,7 +564,7 @@ namespace keepass2android | ||||
| 			MakePasswordMaskedOrVisible(); | ||||
|  | ||||
| 			Handler handler = new Handler(); | ||||
| 			LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, pass, key, new AfterLoad(handler, this)); | ||||
| 			LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, compositeKey, _keyFileOrProvider, new AfterLoad(handler, this)); | ||||
| 			_loadDbTask = null; // prevent accidental re-use | ||||
|  | ||||
| 			SetNewDefaultFile(); | ||||
| @@ -353,6 +572,44 @@ namespace keepass2android | ||||
| 			new ProgressTask(App.Kp2a, this, task).Run(); | ||||
| 		} | ||||
|  | ||||
| 		private void CreateOtpSecret(List<string> lOtps) | ||||
| 		{ | ||||
| 			byte[] pbSecret; | ||||
| 			if (!string.IsNullOrEmpty(_otpInfo.EncryptedSecret)) // < v2.0 | ||||
| 			{ | ||||
| 				byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0, | ||||
| 				                                     lOtps.Count, Convert.FromBase64String( | ||||
| 					                                     _otpInfo.TransformationKey), _otpInfo.TransformationRounds); | ||||
| 				if (pbKey32 == null) throw new InvalidOperationException(); | ||||
|  | ||||
| 				pbSecret = OtpUtil.DecryptData(_otpInfo.EncryptedSecret, | ||||
| 				                               pbKey32, Convert.FromBase64String(_otpInfo.EncryptionIV)); | ||||
| 				if (pbSecret == null) throw new InvalidOperationException(); | ||||
|  | ||||
| 				_otpInfo.Secret = pbSecret; | ||||
| 				_otpInfo.Counter += (ulong) _otpInfo.OtpsRequired; | ||||
| 			} | ||||
| 			else // >= v2.0, supporting look-ahead | ||||
| 			{ | ||||
| 				bool bSuccess = false; | ||||
| 				for (int i = 0; i < _otpInfo.EncryptedSecrets.Count; ++i) | ||||
| 				{ | ||||
| 					OtpEncryptedData d = _otpInfo.EncryptedSecrets[i]; | ||||
| 					pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0, | ||||
| 					                                 lOtps.Count); | ||||
| 					if (pbSecret != null) | ||||
| 					{ | ||||
| 						_otpInfo.Secret = pbSecret; | ||||
| 						_otpInfo.Counter += ((ulong) _otpInfo.OtpsRequired + | ||||
| 						                     (ulong) i); | ||||
| 						bSuccess = true; | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
| 				if (!bSuccess) throw new InvalidOperationException(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		private void MakePasswordMaskedOrVisible() | ||||
| 		{ | ||||
| 			TextView password = (TextView) FindViewById(Resource.Id.password); | ||||
| @@ -521,13 +778,9 @@ namespace keepass2android | ||||
| 		 | ||||
| 		private String GetKeyFile(String filename) { | ||||
| 			if ( _rememberKeyfile ) { | ||||
|                 FileDbHelper dbHelp = App.Kp2a.FileDbHelper; | ||||
| 				 | ||||
| 				String keyfile = dbHelp.GetFileByName(filename); | ||||
| 				 | ||||
| 				return keyfile; | ||||
| 				return App.Kp2a.FileDbHelper.GetKeyFileForFile(filename); | ||||
| 			} else { | ||||
| 				return ""; | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| @@ -541,7 +794,8 @@ namespace keepass2android | ||||
| 			{ | ||||
| 				FindViewById(Resource.Id.filename_group).Visibility = ViewStates.Visible; | ||||
| 			} | ||||
| 			SetEditText(Resource.Id.pass_keyfile, _keyFile); | ||||
| 			if (KeyProviderType == KeyProviders.KeyFile) | ||||
| 				SetEditText(Resource.Id.pass_keyfile, _keyFileOrProvider); | ||||
| 		} | ||||
|  | ||||
| 		protected override void OnDestroy() | ||||
|   | ||||
							
								
								
									
										6687
									
								
								src/keepass2android/Resources/Resource.designer.cs
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6687
									
								
								src/keepass2android/Resources/Resource.designer.cs
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,10 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="fill_parent" | ||||
|     android:layout_height="fill_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:layout_marginLeft="12dip" | ||||
|     android:layout_marginRight="12dip" | ||||
|     android:layout_marginBottom="12dip"> | ||||
|     android:layout_marginBottom="12dip" | ||||
| 	android:orientation="vertical" | ||||
| 			  > | ||||
| 	<RelativeLayout | ||||
| 		android:id="@+id/filename_group" | ||||
| 		android:layout_width="fill_parent" | ||||
| @@ -49,13 +51,18 @@ | ||||
|         android:id="@+id/password_label" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/filename_group" | ||||
|         android:text="" /> | ||||
|     <LinearLayout | ||||
| 	<Spinner | ||||
|     android:id="@+id/password_mode_spinner" | ||||
|     android:layout_width="fill_parent" | ||||
|     android:layout_height="wrap_content"  | ||||
| 	android:entries="@array/password_modes" | ||||
| 	/> | ||||
|  | ||||
| 	<LinearLayout | ||||
|         android:id="@+id/passwordLine" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/password_label" | ||||
|         android:orientation="horizontal"> | ||||
|         <EditText | ||||
|             android:id="@+id/password" | ||||
| @@ -71,11 +78,10 @@ | ||||
|             android:layout_height="wrap_content" | ||||
|             android:src="@drawable/ic_menu_view" /> | ||||
|     </LinearLayout> | ||||
|     <LinearLayout | ||||
| 	<LinearLayout | ||||
|         android:id="@+id/keyfileLine" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/passwordLine" | ||||
|         android:orientation="horizontal"> | ||||
|         <EditText | ||||
|             android:id="@+id/pass_keyfile" | ||||
| @@ -90,22 +96,82 @@ | ||||
|             android:layout_height="wrap_content" | ||||
|             android:src="@drawable/ic_launcher_folder_small" /> | ||||
|     </LinearLayout> | ||||
|     <Button | ||||
| 	<LinearLayout | ||||
|         android:id="@+id/otpView" | ||||
| 		android:layout_marginLeft="12dip" | ||||
| 		android:layout_marginRight="12dip" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
| 		android:orientation="vertical" | ||||
| 		> | ||||
| 		<Button | ||||
|         android:id="@+id/init_otp" | ||||
|         android:text="@string/init_otp" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content"/> | ||||
| 		<LinearLayout | ||||
|         android:id="@+id/otpEntry" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
| 		android:visibility="gone" | ||||
| 		android:orientation="vertical" | ||||
| 		> | ||||
| 			<TextView | ||||
|         android:id="@+id/otp_expl" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:text="@string/otp_explanation" /> | ||||
|  | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp1" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:text="93317749" | ||||
| 						android:singleLine="true" /> | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp2" | ||||
| 						android:text="54719327" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:singleLine="true" /> | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp3" | ||||
| 						android:text="49844651" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:singleLine="true" /> | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp4" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:singleLine="true" /> | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp5" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:singleLine="true" /> | ||||
| 			<EditText | ||||
| 						android:id="@+id/otp6" | ||||
| 						android:layout_width="fill_parent" | ||||
| 						android:layout_height="wrap_content" | ||||
| 						android:singleLine="true" /> | ||||
|  | ||||
| 		</LinearLayout> | ||||
| 		 | ||||
| 	</LinearLayout> | ||||
| 	<Button | ||||
|         android:id="@+id/pass_ok" | ||||
|         android:text="@android:string/ok" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/keyfileLine" /> | ||||
|         android:layout_height="wrap_content"/> | ||||
| 	<Button | ||||
|         android:id="@+id/kill_app" | ||||
|         android:text="@string/kill_app_label" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/pass_ok" /> | ||||
|         android:layout_height="wrap_content" /> | ||||
|     <CheckBox | ||||
|         android:id="@+id/enable_quickunlock" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_below="@id/kill_app" | ||||
|         android:text="@string/enable_quickunlock" /> | ||||
| </RelativeLayout> | ||||
| </LinearLayout> | ||||
| @@ -343,6 +343,14 @@ | ||||
|  | ||||
| 	<string name="error_adding_keyfile">Error while adding the keyfile!</string> | ||||
|  | ||||
| 	<string name="init_otp">Enter OTPs…</string> | ||||
| 	<string name="otp_explanation">Enter the next One-time-passwords (OTPs). Swipe your Yubikey NEO at the back of your device to enter via NFC.</string> | ||||
| 	<string name="otp_hint">OTP %1$d</string> | ||||
| 	<string name="CouldntLoadOtpAuxFile">Could not load auxiliary OTP file!</string> | ||||
|  | ||||
|  | ||||
| 	<string name="loading">Loading…</string> | ||||
| 	 | ||||
| 	<string name="ChangeLog_title">Change log</string> | ||||
|  | ||||
| 	<string name="ChangeLog_0_9_2"> | ||||
| @@ -462,4 +470,9 @@ Initial public release | ||||
|     <item>Remember username only</item> | ||||
|     <item>Remember username and password</item> | ||||
|   </string-array> | ||||
| 	<string-array name="password_modes"> | ||||
| 		<item>Password only</item> | ||||
| 		<item>Password + Key file</item> | ||||
| 		<item>Password + OTP</item> | ||||
| 	</string-array> | ||||
| </resources> | ||||
|   | ||||
							
								
								
									
										164
									
								
								src/keepass2android/Utils/LoadingDialog.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/keepass2android/Utils/LoadingDialog.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| using System; | ||||
| using Android.App; | ||||
| using Android.Content; | ||||
| using Android.OS; | ||||
| using Android.Runtime; | ||||
| using Java.Lang; | ||||
| using Exception = System.Exception; | ||||
| using Object = Java.Lang.Object; | ||||
|  | ||||
| namespace keepass2android.Utils | ||||
| { | ||||
| 	public class LoadingDialog<TParams, TProgress, TResult> : AsyncTask<TParams, TProgress, TResult>  | ||||
| 	{ | ||||
| 		private readonly Context _context; | ||||
| 		private readonly string _message; | ||||
| 		private readonly bool _cancelable; | ||||
| 		readonly Func<Object[], Object> _doInBackground; | ||||
| 		readonly Action<Object> _onPostExecute; | ||||
|  | ||||
| 		private ProgressDialog mDialog; | ||||
| 		/** | ||||
| 		 * Default is {@code 500}ms | ||||
| 		 */ | ||||
| 		private int mDelayTime = 500; | ||||
| 		/** | ||||
| 		 * Flag to use along with {@link #mDelayTime} | ||||
| 		 */ | ||||
| 		private bool mFinished = false; | ||||
|  | ||||
| 		private Exception mLastException; | ||||
|  | ||||
| 		 | ||||
| 		public LoadingDialog(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) | ||||
| 		{ | ||||
| 		} | ||||
|  | ||||
| 		public LoadingDialog(Context context, string message, bool cancelable, Func<Object[], Object> doInBackground,  | ||||
| 			Action<Object> onPostExecute) | ||||
| 		{ | ||||
| 			_context = context; | ||||
| 			_message = message; | ||||
| 			_cancelable = cancelable; | ||||
| 			_doInBackground = doInBackground; | ||||
| 			_onPostExecute = onPostExecute; | ||||
| 			Initialize(); | ||||
| 		} | ||||
|  | ||||
| 		private void Initialize() | ||||
| 		{ | ||||
| 			mDialog = new ProgressDialog(_context); | ||||
| 			mDialog.SetMessage(_message); | ||||
| 			mDialog.Indeterminate = true; | ||||
| 			mDialog.SetCancelable(_cancelable); | ||||
| 			if (_cancelable) | ||||
| 			{ | ||||
| 				mDialog.SetCanceledOnTouchOutside(true); | ||||
| 				mDialog.CancelEvent += (sender, args) => mDialog.Cancel(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public LoadingDialog(Context context, bool cancelable, Func<Object[], Object> doInBackground, Action<Object> onPostExecute) | ||||
| 		{ | ||||
| 			_message = context.GetString(Resource.String.loading); | ||||
| 			_context = context; | ||||
| 			_cancelable = cancelable; | ||||
| 			_doInBackground = doInBackground; | ||||
| 			_onPostExecute = onPostExecute; | ||||
| 			Initialize(); | ||||
| 		} | ||||
|  | ||||
| 		protected override void OnPreExecute() | ||||
| 		{ | ||||
| 			new Handler().PostDelayed(() => | ||||
| 				{ | ||||
| 					if (!mFinished) | ||||
| 					{ | ||||
| 						try | ||||
| 						{ | ||||
| 							/* | ||||
| 							 * sometime the activity has been finished before we | ||||
| 							 * show this dialog, it will raise error | ||||
| 							 */ | ||||
| 							mDialog.Show(); | ||||
| 						} | ||||
| 						catch (Exception t) | ||||
| 						{ | ||||
| 							Kp2aLog.Log(t.ToString()); | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 				} | ||||
| 				, mDelayTime); | ||||
| 		} | ||||
| 		 | ||||
|    | ||||
| 		/** | ||||
| 		 * If you override this method, you must call {@code super.onCancelled()} at | ||||
| 		 * beginning of the method. | ||||
| 		 */ | ||||
| 		protected override void OnCancelled() { | ||||
| 			DoFinish(); | ||||
| 			base.OnCancelled(); | ||||
| 		}// onCancelled() | ||||
|  | ||||
| 		private void DoFinish() { | ||||
| 			mFinished = true; | ||||
| 			try { | ||||
| 				/* | ||||
| 				 * Sometime the activity has been finished before we dismiss this | ||||
| 				 * dialog, it will raise error. | ||||
| 				 */ | ||||
| 				mDialog.Dismiss(); | ||||
| 			} catch (Exception e) | ||||
| 			{ | ||||
| 				Kp2aLog.Log(e.ToString()); | ||||
| 			} | ||||
| 		}// doFinish() | ||||
|  | ||||
|  | ||||
| 		/** | ||||
| 		 * Sets last exception. This method is useful in case an exception raises | ||||
| 		 * inside {@link #doInBackground(Void...)} | ||||
| 		 *  | ||||
| 		 * @param t | ||||
| 		 *            {@link Throwable} | ||||
| 		 */ | ||||
| 		protected void SetLastException(Exception e) { | ||||
| 			mLastException = e; | ||||
| 		}// setLastException() | ||||
|  | ||||
| 		/** | ||||
| 		 * Gets last exception. | ||||
| 		 *  | ||||
| 		 * @return {@link Throwable} | ||||
| 		 */ | ||||
| 		protected Exception GetLastException() { | ||||
| 			return mLastException; | ||||
| 		}// getLastException() | ||||
|  | ||||
|  | ||||
| 		protected override Object DoInBackground(params Object[] @params) | ||||
| 		{ | ||||
| 			return _doInBackground(@params); | ||||
| 		} | ||||
|  | ||||
| 		protected override TResult RunInBackground(params TParams[] @params) | ||||
| 		{ | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
|  | ||||
| 		protected override void OnPostExecute(Object result) | ||||
| 		{ | ||||
| 			DoFinish(); | ||||
| 			 | ||||
| 			if (_onPostExecute != null) | ||||
| 				_onPostExecute(result); | ||||
| 		} | ||||
|  | ||||
| 		 | ||||
| 		 | ||||
| 		 | ||||
|  | ||||
| 	} | ||||
| } | ||||
| @@ -20,7 +20,6 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using System.Windows.Forms; | ||||
| using System.Diagnostics; | ||||
|  | ||||
| using KeePassLib.Utility; | ||||
| @@ -39,9 +38,8 @@ namespace OtpKeyProv | ||||
| 			FmtHex, FmtBase64, FmtBase32, FmtUtf8, FmtDec | ||||
| 		}; | ||||
|  | ||||
| 		public static OtpDataFmt? GetOtpDataFormat(ComboBox cmb) | ||||
| 		public static OtpDataFmt? GetOtpDataFormat(String strFmt) | ||||
| 		{ | ||||
| 			string strFmt = (cmb.SelectedItem as string); | ||||
| 			if(strFmt == null) return null; // No assert | ||||
|  | ||||
| 			if(strFmt == FmtHex) return OtpDataFmt.Hex; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| /* | ||||
|   This file was modified my Philipp Crocoll, 2013. Based on:  | ||||
|    | ||||
|   OtpKeyProv Plugin | ||||
|   Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de> | ||||
|  | ||||
| @@ -19,81 +21,49 @@ | ||||
|  | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using System.Windows.Forms; | ||||
| using System.Diagnostics; | ||||
|  | ||||
| using OtpKeyProv.Forms; | ||||
|  | ||||
| using KeePass.UI; | ||||
|  | ||||
| using KeePassLib.Keys; | ||||
| using KeePassLib.Serialization; | ||||
| using KeePassLib.Utility; | ||||
| using keepass2android; | ||||
| using keepass2android.Io; | ||||
|  | ||||
| namespace OtpKeyProv | ||||
| { | ||||
| 	public sealed class OathHotpKeyProv : KeyProvider | ||||
| 	public sealed class OathHotpKeyProv | ||||
| 		/*removed base class KeyProvider because "synchronous" interface is not suitable on Android*/ | ||||
| 	{ | ||||
| 		private const string AuxFileExt = ".otp.xml"; | ||||
| 		private const string ProvType = "OATH HOTP / RFC 4226"; | ||||
| 		private const string ProvVersion = "2.0"; // File version, not OtpKeyProv version | ||||
|  | ||||
| 		public override string Name | ||||
| 		public static string Name | ||||
| 		{ | ||||
| 			get { return "One-Time Passwords (OATH HOTP)"; } | ||||
| 		} | ||||
|  | ||||
| 		public override bool SecureDesktopCompatible | ||||
| 		{ | ||||
| 			get { return true; } | ||||
| 		} | ||||
|  | ||||
| 		public override byte[] GetKey(KeyProviderQueryContext ctx) | ||||
| 		{ | ||||
| 			try | ||||
| 			{ | ||||
| 				if(ctx.CreatingNewKey) return Create(ctx); | ||||
| 				return Open(ctx); | ||||
| 			} | ||||
| 			catch(Exception ex) { MessageService.ShowWarning(ex.Message); } | ||||
| 		public const string ShortProductName = "OtpKeyProv"; | ||||
| 		public const string ProductName = "OtpKeyProv KeePass Plugin"; | ||||
|  | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx) | ||||
| 		{ | ||||
| 			IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep(); | ||||
| 			ioc.Path = UrlUtil.StripExtension(ioc.Path) + AuxFileExt; | ||||
| 			return ioc; | ||||
| 			IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(ioc); | ||||
| 			IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(ioc), | ||||
| 			                                                  fileStorage.GetFilenameWithoutPathAndExt(ioc) + AuxFileExt); | ||||
|  | ||||
| 			return iocAux; | ||||
| 		} | ||||
|  | ||||
| 		private static byte[] Create(KeyProviderQueryContext ctx) | ||||
| 		public static OtpInfo LoadOtpInfo(KeyProviderQueryContext ctx) | ||||
| 		{ | ||||
| 			IOConnectionInfo iocPrev = GetAuxFileIoc(ctx); | ||||
| 			OtpInfo otpInfo = OtpInfo.Load(iocPrev); | ||||
| 			if(otpInfo == null) otpInfo = new OtpInfo(); | ||||
|  | ||||
| 			OtpKeyCreationForm dlg = new OtpKeyCreationForm(); | ||||
| 			dlg.InitEx(otpInfo, ctx); | ||||
|  | ||||
| 			if(UIUtil.ShowDialogAndDestroy(dlg) != DialogResult.OK) | ||||
| 				return null; | ||||
|  | ||||
| 			if(!CreateAuxFile(otpInfo, ctx)) return null; | ||||
| 			return otpInfo.Secret; | ||||
| 			return OtpInfo.Load(GetAuxFileIoc(ctx)); | ||||
| 		} | ||||
|  | ||||
| 		private static byte[] Open(KeyProviderQueryContext ctx) | ||||
| 		/* | ||||
| 		private static byte[] Open(KeyProviderQueryContext ctx, OtpInfo otpInfo) | ||||
| 		{ | ||||
| 			IOConnectionInfo ioc = GetAuxFileIoc(ctx); | ||||
| 			OtpInfo otpInfo = OtpInfo.Load(ioc); | ||||
| 			if(otpInfo == null) | ||||
| 			{ | ||||
| 				MessageService.ShowWarning("Failed to load auxiliary OTP info file:", | ||||
| 					ioc.GetDisplayName()); | ||||
| 				return null; | ||||
| 			} | ||||
| 			if(otpInfo.Type != ProvType) | ||||
| 			{ | ||||
| 				MessageService.ShowWarning("Unknown OTP generator type!"); | ||||
| @@ -108,13 +78,72 @@ namespace OtpKeyProv | ||||
| 			if(!CreateAuxFile(otpInfo, ctx)) return null; | ||||
| 			return otpInfo.Secret; | ||||
| 		} | ||||
| 		 * */ | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// Sets the "Secret" field in otpInfo based on the list of entered OTPs (lOtps) or the entered secret itself which is in format fmt | ||||
| 		/// </summary> | ||||
| 		/// based on the code in OtpKeyPromptForm.cs | ||||
| 		public void SetSecret(OtpInfo otpInfo, List<string> lOtps, string secret, OtpDataFmt? fmt) | ||||
| 		{ | ||||
| 			try | ||||
| 			{ | ||||
| 				byte[] pbSecret = EncodingUtil.ParseKey(secret, | ||||
| 				                                        (fmt.HasValue ? fmt.Value : OtpDataFmt.Hex)); | ||||
| 				if (pbSecret != null) | ||||
| 				{ | ||||
| 					otpInfo.Secret = pbSecret; | ||||
| 					return; | ||||
| 				} | ||||
|  | ||||
| 				if (!string.IsNullOrEmpty(otpInfo.EncryptedSecret)) // < v2.0 | ||||
| 				{ | ||||
| 					byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0, | ||||
| 					                                     lOtps.Count, Convert.FromBase64String( | ||||
| 						                                     otpInfo.TransformationKey), otpInfo.TransformationRounds); | ||||
| 					if (pbKey32 == null) throw new InvalidOperationException(); | ||||
|  | ||||
| 					pbSecret = OtpUtil.DecryptData(otpInfo.EncryptedSecret, | ||||
| 					                               pbKey32, Convert.FromBase64String(otpInfo.EncryptionIV)); | ||||
| 					if (pbSecret == null) throw new InvalidOperationException(); | ||||
|  | ||||
| 					otpInfo.Secret = pbSecret; | ||||
| 					otpInfo.Counter += (ulong) otpInfo.OtpsRequired; | ||||
| 				} | ||||
| 				else // >= v2.0, supporting look-ahead | ||||
| 				{ | ||||
| 					bool bSuccess = false; | ||||
| 					for (int i = 0; i < otpInfo.EncryptedSecrets.Count; ++i) | ||||
| 					{ | ||||
| 						OtpEncryptedData d = otpInfo.EncryptedSecrets[i]; | ||||
| 						pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0, | ||||
| 						                                 lOtps.Count); | ||||
| 						if (pbSecret != null) | ||||
| 						{ | ||||
| 							otpInfo.Secret = pbSecret; | ||||
| 							otpInfo.Counter += ((ulong) otpInfo.OtpsRequired + | ||||
| 							                      (ulong) i); | ||||
| 							bSuccess = true; | ||||
| 							break; | ||||
| 						} | ||||
| 					} | ||||
| 					if (!bSuccess) throw new InvalidOperationException(); | ||||
| 				} | ||||
| 			} | ||||
| 			catch (Exception) | ||||
| 			{ | ||||
| 				//todo | ||||
| 				throw; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
|  | ||||
| 		private static bool CreateAuxFile(OtpInfo otpInfo, | ||||
| 			KeyProviderQueryContext ctx) | ||||
| 		{ | ||||
| 			otpInfo.Type = ProvType; | ||||
| 			otpInfo.Version = ProvVersion; | ||||
| 			otpInfo.Generator = OtpKeyProvExt.ProductName; | ||||
| 			otpInfo.Generator = ProductName; | ||||
|  | ||||
| 			otpInfo.EncryptSecret(); | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| /* | ||||
|   This file was modified my Philipp Crocoll, 2013. Based on:  | ||||
|  | ||||
|   OtpKeyProv Plugin | ||||
|   Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de> | ||||
|  | ||||
| @@ -31,6 +33,7 @@ using KeePassLib.Cryptography; | ||||
| using KeePassLib.Keys; | ||||
| using KeePassLib.Serialization; | ||||
| using KeePassLib.Utility; | ||||
| using keepass2android; | ||||
|  | ||||
| namespace OtpKeyProv | ||||
| { | ||||
| @@ -168,12 +171,15 @@ namespace OtpKeyProv | ||||
|  | ||||
| 			try | ||||
| 			{ | ||||
| 				sIn = IOConnection.OpenRead(ioc); | ||||
| 				sIn = App.Kp2a.GetOtpAuxFileStorage(ioc).OpenFileForRead(ioc); | ||||
|  | ||||
| 				XmlSerializer xs = new XmlSerializer(typeof(OtpInfo)); | ||||
| 				return (OtpInfo)xs.Deserialize(sIn); | ||||
| 				XmlSerializer xs = new XmlSerializer(typeof (OtpInfo)); | ||||
| 				return (OtpInfo) xs.Deserialize(sIn); | ||||
| 			} | ||||
| 			catch (Exception e) | ||||
| 			{ | ||||
| 				Kp2aLog.Log(e.ToString()); | ||||
| 			} | ||||
| 			catch(Exception) { } | ||||
| 			finally | ||||
| 			{ | ||||
| 				if(sIn != null) sIn.Close(); | ||||
| @@ -188,20 +194,23 @@ namespace OtpKeyProv | ||||
|  | ||||
| 			try | ||||
| 			{ | ||||
| 				sOut = IOConnection.OpenWrite(ioc); | ||||
| 				using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc) | ||||
| 					               .OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions))) | ||||
| 				{ | ||||
| 					XmlWriterSettings xws = new XmlWriterSettings(); | ||||
| 					xws.CloseOutput = true; | ||||
| 					xws.Encoding = StrUtil.Utf8; | ||||
| 					xws.Indent = true; | ||||
| 					xws.IndentChars = "\t"; | ||||
|  | ||||
| 				XmlWriterSettings xws = new XmlWriterSettings(); | ||||
| 				xws.CloseOutput = true; | ||||
| 				xws.Encoding = StrUtil.Utf8; | ||||
| 				xws.Indent = true; | ||||
| 				xws.IndentChars = "\t"; | ||||
| 					XmlWriter xw = XmlWriter.Create(trans.OpenFile(), xws); | ||||
|  | ||||
| 				XmlWriter xw = XmlWriter.Create(sOut, xws); | ||||
| 					XmlSerializer xs = new XmlSerializer(typeof (OtpInfo)); | ||||
| 					xs.Serialize(xw, otpInfo); | ||||
|  | ||||
| 				XmlSerializer xs = new XmlSerializer(typeof(OtpInfo)); | ||||
| 				xs.Serialize(xw, otpInfo); | ||||
|  | ||||
| 				xw.Close(); | ||||
| 					xw.Close(); | ||||
| 					trans.CommitWrite(); | ||||
| 				} | ||||
| 				return true; | ||||
| 			} | ||||
| 			catch(Exception) { Debug.Assert(false); } | ||||
|   | ||||
| @@ -107,9 +107,9 @@ namespace keepass2android | ||||
| 			Application.Context.SendBroadcast(new Intent(Intents.DatabaseLocked)); | ||||
|         } | ||||
|  | ||||
| 		public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, string password, string keyFile, ProgressDialogStatusLogger statusLogger) | ||||
| 		public void LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, ProgressDialogStatusLogger statusLogger) | ||||
| 		{ | ||||
| 			_db.LoadData(this, ioConnectionInfo, memoryStream, password, keyFile, statusLogger); | ||||
| 			_db.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger); | ||||
|  | ||||
| 			UpdateOngoingNotification(); | ||||
| 		} | ||||
| @@ -354,10 +354,9 @@ namespace keepass2android | ||||
| 			{ | ||||
| 				IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); | ||||
|  | ||||
| 				var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); | ||||
|  | ||||
| 				if (prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key), true)) | ||||
| 				if (DatabaseCacheEnabled) | ||||
| 				{ | ||||
| 					//TODO | ||||
| 					return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);	 | ||||
| 				} | ||||
| 				else | ||||
| @@ -494,6 +493,42 @@ namespace keepass2android | ||||
| 		{ | ||||
| 			return GetFileStorage(new IOConnectionInfo() {Path = protocolId + "://"}); | ||||
| 		} | ||||
|  | ||||
| 		/// <summary> | ||||
| 		/// returns a file storage object to be used when accessing the auxiliary OTP file | ||||
| 		/// </summary> | ||||
| 		/// The reason why this requires a different file storage is the different caching behavior. | ||||
| 		public IFileStorage GetOtpAuxFileStorage(IOConnectionInfo iocInfo) | ||||
| 		{ | ||||
|  | ||||
| 			if (iocInfo.IsLocalFile()) | ||||
| 				return new BuiltInFileStorage(); | ||||
| 			else | ||||
| 			{ | ||||
| 				IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); | ||||
|  | ||||
| 				 | ||||
| 				if (DatabaseCacheEnabled) | ||||
| 				{ | ||||
| 					return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					return innerFileStorage; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		private static bool DatabaseCacheEnabled | ||||
| 		{ | ||||
| 			get | ||||
| 			{ | ||||
| 				var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context); | ||||
| 				bool cacheEnabled = prefs.GetBoolean(Application.Context.Resources.GetString(Resource.String.UseOfflineCache_key), | ||||
| 				                                     true); | ||||
| 				return cacheEnabled; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -230,12 +230,12 @@ namespace keepass2android | ||||
|  | ||||
| 		} | ||||
| 		 | ||||
| 		public String GetFileByName(String name) { | ||||
| 		public String GetKeyFileForFile(String name) { | ||||
| 			ICursor cursor = mDb.Query(true, FileTable, GetColumnList(), | ||||
| 			KeyFileFilename + "= ?", new[] {name}, null, null, null, null); | ||||
| 			 | ||||
| 			if ( cursor == null ) { | ||||
| 				return ""; | ||||
| 				return null; | ||||
| 			} | ||||
| 			 | ||||
| 			String keyfileFilename; | ||||
| @@ -244,9 +244,11 @@ namespace keepass2android | ||||
| 				keyfileFilename = cursor.GetString(cursor.GetColumnIndexOrThrow(KeyFileKeyfile)); | ||||
| 			} else { | ||||
| 				// Cursor is empty | ||||
| 				keyfileFilename = ""; | ||||
| 				keyfileFilename = null; | ||||
| 			} | ||||
| 			cursor.Close(); | ||||
| 			if (keyfileFilename == "") | ||||
| 				return null; | ||||
| 			return keyfileFilename; | ||||
| 		} | ||||
| 		 | ||||
|   | ||||
| @@ -373,7 +373,7 @@ namespace keepass2android | ||||
| #if !EXCLUDE_FILECHOOSER | ||||
| 				StartFileChooser(ioc.Path); | ||||
| #else | ||||
| 				LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/keepass.kdbx"}); | ||||
| 				LaunchPasswordActivityForIoc(new IOConnectionInfo { Path = "/mnt/sdcard/keepass/yubi2.kdbx"}); | ||||
| #endif | ||||
| 			} | ||||
| 			if ((resultCode == Result.Canceled) && (data != null) && (data.HasExtra("EXTRA_ERROR_MESSAGE"))) | ||||
|   | ||||
| @@ -82,6 +82,10 @@ | ||||
|     <Reference Include="Mono.Android.Support.v4" /> | ||||
|   </ItemGroup> | ||||
|   <ItemGroup> | ||||
|     <Compile Include="addons\OtpKeyProv\EncodingUtil.cs" /> | ||||
|     <Compile Include="addons\OtpKeyProv\OathHotpKeyProv.cs" /> | ||||
|     <Compile Include="addons\OtpKeyProv\OtpInfo.cs" /> | ||||
|     <Compile Include="addons\OtpKeyProv\OtpUtil.cs" /> | ||||
|     <Compile Include="app\NoFileStorageFoundException.cs" /> | ||||
|     <Compile Include="CreateDatabaseActivity.cs" /> | ||||
|     <Compile Include="fileselect\FileChooserFileProvider.cs" /> | ||||
| @@ -102,6 +106,7 @@ | ||||
|     <Compile Include="search\SearchProvider.cs" /> | ||||
|     <Compile Include="services\OngoingNotificationsService.cs" /> | ||||
|     <Compile Include="settings\DatabaseSettingsActivity.cs" /> | ||||
|     <Compile Include="Utils\LoadingDialog.cs" /> | ||||
|     <Compile Include="Utils\Util.cs" /> | ||||
|     <Compile Include="intents\Intents.cs" /> | ||||
|     <Compile Include="timeout\TimeoutHelper.cs" /> | ||||
| @@ -629,7 +634,9 @@ | ||||
|     <AndroidResource Include="Resources\layout-v14\SaveButton.xml" /> | ||||
|     <AndroidResource Include="Resources\layout-v14\generate_password.xml" /> | ||||
|     <AndroidResource Include="Resources\layout-v14\icon_picker.xml" /> | ||||
|     <AndroidResource Include="Resources\layout-v14\password.xml" /> | ||||
|     <AndroidResource Include="Resources\layout-v14\password.xml"> | ||||
|       <SubType>Designer</SubType> | ||||
|     </AndroidResource> | ||||
|     <AndroidResource Include="Resources\layout\InViewButton.xml" /> | ||||
|     <AndroidResource Include="Resources\drawable\collections_collection.png" /> | ||||
|     <AndroidResource Include="Resources\drawable\collections_new_label.png" /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Philipp Crocoll
					Philipp Crocoll