From ec4fe32b29b87edb72039fff083c3a32e7f5d632 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Sun, 27 Dec 2015 08:50:45 +0100 Subject: [PATCH] added offline mode --- .../Io/CachingFileStorage.cs | 14 +- .../Io/OfflineSwitchableFileStorage.cs | 191 +++++ .../Kp2aBusinessLogic.csproj | 1 + src/Kp2aBusinessLogic/UiStringKey.cs | 3 +- src/keepass2android/GroupBaseActivity.cs | 736 ++++++++++-------- src/keepass2android/PasswordActivity.cs | 40 +- .../Resources/layout/password.xml | 39 + src/keepass2android/Resources/menu/group.xml | 8 + .../Resources/values/config.xml | 4 + .../Resources/values/strings.xml | 9 +- .../Resources/values/styles.xml | 15 +- src/keepass2android/app/App.cs | 53 +- .../fileselect/FileSelectActivity.cs | 1 + .../views/Kp2aShortHelpView.cs | 8 +- 14 files changed, 776 insertions(+), 346 deletions(-) create mode 100644 src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs index d20bbd56..4a0ee35b 100644 --- a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -54,19 +54,21 @@ namespace keepass2android.Io void LoadedFromRemoteInSync(IOConnectionInfo ioc); } + /// /// Implements the IFileStorage interface as a proxy: A base storage is used as a remote storage. Local files are used to cache the /// files on remote. /// - public class CachingFileStorage: IFileStorage + public class CachingFileStorage : IFileStorage, IOfflineSwitchable { - protected readonly IFileStorage _cachedStorage; + + protected readonly OfflineSwitchableFileStorage _cachedStorage; private readonly ICacheSupervisor _cacheSupervisor; private readonly string _streamCacheDir; public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor) { - _cachedStorage = cachedStorage; + _cachedStorage = new OfflineSwitchableFileStorage(cachedStorage); _cacheSupervisor = cacheSupervisor; _streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator; if (!Directory.Exists(_streamCacheDir)) @@ -610,5 +612,11 @@ namespace keepass2android.Io return File.OpenRead(CachedFilePath(ioc)); } } + + public bool IsOffline + { + get { return _cachedStorage.IsOffline; } + set { _cachedStorage.IsOffline = value; } + } } } diff --git a/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs new file mode 100644 index 00000000..8f492da7 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Android.Content; +using Android.OS; +using KeePassLib.Serialization; + +namespace keepass2android.Io +{ + public interface IOfflineSwitchable + { + bool IsOffline { get; set; } + } + +/// + /// Encapsulates another IFileStorage. Allows to switch to offline mode by throwing + /// an exception when trying to read or write a file. + /// + public class OfflineSwitchableFileStorage : IFileStorage, IOfflineSwitchable + { + private readonly IFileStorage _baseStorage; + public bool IsOffline { get; set; } + + public OfflineSwitchableFileStorage(IFileStorage baseStorage) + { + _baseStorage = baseStorage; + } + + public IEnumerable SupportedProtocols + { + get { return _baseStorage.SupportedProtocols; } + } + + public void Delete(IOConnectionInfo ioc) + { + _baseStorage.Delete(ioc); + } + + public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) + { + return _baseStorage.CheckForFileChangeFast(ioc, previousFileVersion); + } + + public string GetCurrentFileVersionFast(IOConnectionInfo ioc) + { + return _baseStorage.GetCurrentFileVersionFast(ioc); + } + + public Stream OpenFileForRead(IOConnectionInfo ioc) + { + AssertOnline(); + return _baseStorage.OpenFileForRead(ioc); + } + + private void AssertOnline() + { + if (IsOffline) + { + //throw new Exception(_app.GetResourceString(UiStringKey.InOfflineMode)); + throw new OfflineModeException(); + } + } + + public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + AssertOnline(); + return _baseStorage.OpenWriteTransaction(ioc, useFileTransaction); + } + + public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) + { + return _baseStorage.GetFilenameWithoutPathAndExt(ioc); + } + + public bool RequiresCredentials(IOConnectionInfo ioc) + { + return _baseStorage.RequiresCredentials(ioc); + } + + public void CreateDirectory(IOConnectionInfo ioc, string newDirName) + { + _baseStorage.CreateDirectory(ioc, newDirName); + } + + public IEnumerable ListContents(IOConnectionInfo ioc) + { + return _baseStorage.ListContents(ioc); + } + + public FileDescription GetFileDescription(IOConnectionInfo ioc) + { + return _baseStorage.GetFileDescription(ioc); + } + + public bool RequiresSetup(IOConnectionInfo ioConnection) + { + if (IsOffline) + return false; + return _baseStorage.RequiresSetup(ioConnection); + } + + public string IocToPath(IOConnectionInfo ioc) + { + return _baseStorage.IocToPath(ioc); + } + + public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) + { + _baseStorage.StartSelectFile(activity, isForSave, requestCode, protocolId); + } + + public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, + bool alwaysReturnSuccess) + { + if (IsOffline) + { + Intent intent = new Intent(); + activity.IocToIntent(intent, ioc); + activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); + return; + } + + _baseStorage.PrepareFileUsage(activity, ioc, requestCode, alwaysReturnSuccess); + } + + public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) + { + if (IsOffline) + return; + _baseStorage.PrepareFileUsage(ctx, ioc); + } + + public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) + { + _baseStorage.OnCreate(activity, savedInstanceState); + } + + public void OnResume(IFileStorageSetupActivity activity) + { + _baseStorage.OnResume(activity); + } + + public void OnStart(IFileStorageSetupActivity activity) + { + _baseStorage.OnStart(activity); + } + + public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) + { + _baseStorage.OnActivityResult(activity, requestCode, resultCode, data); + } + + public string GetDisplayName(IOConnectionInfo ioc) + { + return _baseStorage.GetDisplayName(ioc); + } + + public string CreateFilePath(string parent, string newFilename) + { + return _baseStorage.CreateFilePath(parent, newFilename); + } + + public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) + { + return _baseStorage.GetParentPath(ioc); + } + + public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) + { + return _baseStorage.GetFilePath(folderPath, filename); + } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return _baseStorage.IsPermanentLocation(ioc); + } + + public bool IsReadOnly(IOConnectionInfo ioc) + { + return _baseStorage.IsReadOnly(ioc); + } + } + + public class OfflineModeException : Exception + { + public override string Message + { + get { return "Working offline."; } + } + } +} \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index c899a0a6..d49b3253 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -79,6 +79,7 @@ + diff --git a/src/Kp2aBusinessLogic/UiStringKey.cs b/src/Kp2aBusinessLogic/UiStringKey.cs index 92cd9f37..614897d8 100644 --- a/src/Kp2aBusinessLogic/UiStringKey.cs +++ b/src/Kp2aBusinessLogic/UiStringKey.cs @@ -61,6 +61,7 @@ namespace keepass2android DuplicateUuidsErrorAdditional, DeletingItems, AskDeletePermanentlyItems, - AskDeletePermanentlyItemsNoRecycle + AskDeletePermanentlyItemsNoRecycle, + InOfflineMode } } diff --git a/src/keepass2android/GroupBaseActivity.cs b/src/keepass2android/GroupBaseActivity.cs index 60bc6aa6..37a42588 100644 --- a/src/keepass2android/GroupBaseActivity.cs +++ b/src/keepass2android/GroupBaseActivity.cs @@ -40,8 +40,9 @@ using Object = Java.Lang.Object; namespace keepass2android { - - public abstract class GroupBaseActivity : LockCloseActivity { + + public abstract class GroupBaseActivity : LockCloseActivity + { public const String KeyEntry = "entry"; public const String KeyMode = "mode"; @@ -52,15 +53,15 @@ namespace keepass2android EntryActivity.Launch(this, pwEntry, pos, AppTask); } - protected GroupBaseActivity () + protected GroupBaseActivity() { _design = new ActivityDesign(this); } - protected GroupBaseActivity (IntPtr javaReference, JniHandleOwnership transfer) + protected GroupBaseActivity(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { - + } protected override void OnSaveInstanceState(Bundle outState) @@ -71,21 +72,21 @@ namespace keepass2android public virtual void SetupNormalButtons() { - SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled); + SetNormalButtonVisibility(AddGroupEnabled, AddEntryEnabled); } - protected virtual bool AddGroupEnabled - { - get { return App.Kp2a.GetDb().CanWrite; } - } - protected virtual bool AddEntryEnabled - { - get { return App.Kp2a.GetDb().CanWrite; } - } + protected virtual bool AddGroupEnabled + { + get { return App.Kp2a.GetDb().CanWrite; } + } + protected virtual bool AddEntryEnabled + { + get { return App.Kp2a.GetDb().CanWrite; } + } - public void SetNormalButtonVisibility(bool showAddGroup, bool showAddEntry) - { + public void SetNormalButtonVisibility(bool showAddGroup, bool showAddEntry) + { //check for null in the following because the "empty" layouts may not have all views if (FindViewById(Resource.Id.bottom_bar) != null) @@ -94,17 +95,17 @@ namespace keepass2android if (FindViewById(Resource.Id.divider2) != null) FindViewById(Resource.Id.divider2).Visibility = BottomBarAlwaysVisible ? ViewStates.Visible : ViewStates.Gone; - if (FindViewById(Resource.Id.fabCancelAddNew) != null) - { + if (FindViewById(Resource.Id.fabCancelAddNew) != null) + { FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = (showAddGroup || showAddEntry) ? ViewStates.Visible : ViewStates.Gone; - } - - - } + FindViewById(Resource.Id.fabAddNew).Visibility = (showAddGroup || showAddEntry) ? ViewStates.Visible : ViewStates.Gone; + } + + + } public virtual bool BottomBarAlwaysVisible { @@ -140,8 +141,8 @@ namespace keepass2android else { PwUuid groupUuid = new PwUuid(MemUtil.HexStringToByteArray(strGroupUuid)); - task = new EditGroup(this, App.Kp2a, groupName, (PwIcon) groupIconId, groupCustomIconId, App.Kp2a.GetDb().Groups[groupUuid], - new RefreshTask(handler, this)); + task = new EditGroup(this, App.Kp2a, groupName, (PwIcon)groupIconId, groupCustomIconId, App.Kp2a.GetDb().Groups[groupUuid], + new RefreshTask(handler, this)); } ProgressTask pt = new ProgressTask(App.Kp2a, act, task); pt.Run(); @@ -160,33 +161,39 @@ namespace keepass2android } } - + private ISharedPreferences _prefs; - + protected PwGroup Group; internal AppTask AppTask; - + private String strCachedGroupUuid = null; - + private IMenuItem _offlineItem; + private IMenuItem _onlineItem; + private IMenuItem _syncItem; - public String UuidGroup { - get { - if (strCachedGroupUuid == null) { - strCachedGroupUuid = MemUtil.ByteArrayToHexString (Group.Uuid.UuidBytes); + public String UuidGroup + { + get + { + if (strCachedGroupUuid == null) + { + strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes); } return strCachedGroupUuid; } } - protected override void OnResume() { + protected override void OnResume() + { base.OnResume(); _design.ReapplyTheme(); AppTask.StartInGroupActivity(this); AppTask.SetupGroupBaseActivityButtons(this); - + RefreshIfDirty(); } @@ -198,46 +205,50 @@ namespace keepass2android return true; } - public void RefreshIfDirty() { + public void RefreshIfDirty() + { Database db = App.Kp2a.GetDb(); - if ( db.Dirty.Contains(Group) ) { + if (db.Dirty.Contains(Group)) + { db.Dirty.Remove(Group); ListAdapter.NotifyDataSetChanged(); - + } } - public BaseAdapter ListAdapter - { - get { return (BaseAdapter) FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListAdapter; } - } + public BaseAdapter ListAdapter + { + get { return (BaseAdapter)FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListAdapter; } + } - public virtual bool IsSearchResult - { - get { return false; } - } + public virtual bool IsSearchResult + { + get { return false; } + } - protected override void OnCreate(Bundle savedInstanceState) { - _design.ApplyTheme(); + protected override void OnCreate(Bundle savedInstanceState) + { + _design.ApplyTheme(); base.OnCreate(savedInstanceState); - + Android.Util.Log.Debug("KP2A", "Creating GBA"); AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent); - + // Likely the app has been killed exit the activity - if ( ! App.Kp2a.GetDb().Loaded ) { + if (!App.Kp2a.GetDb().Loaded) + { Finish(); return; } - + _prefs = PreferenceManager.GetDefaultSharedPreferences(this); - + SetContentView(ContentResourceId); - if (FindViewById(Resource.Id.fabCancelAddNew) != null) - { + if (FindViewById(Resource.Id.fabCancelAddNew) != null) + { FindViewById(Resource.Id.fabAddNew).Click += (sender, args) => { FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Visible; @@ -254,21 +265,21 @@ namespace keepass2android FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Visible; }; - - } + + } - if (FindViewById(Resource.Id.cancel_insert_element) != null) - { + if (FindViewById(Resource.Id.cancel_insert_element) != null) + { FindViewById(Resource.Id.cancel_insert_element).Click += (sender, args) => StopMovingElements(); - FindViewById(Resource.Id.insert_element).Click += (sender, args) => InsertElements(); - } - - - SetResult(KeePass.ExitNormal); - - - + FindViewById(Resource.Id.insert_element).Click += (sender, args) => InsertElements(); + } + + + SetResult(KeePass.ExitNormal); + + + } protected virtual int ContentResourceId @@ -279,19 +290,19 @@ namespace keepass2android private void InsertElements() { MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; - IEnumerable elementsToMove = - moveElementsTask.Uuids.Select(uuid => App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null)); - + IEnumerable elementsToMove = + moveElementsTask.Uuids.Select(uuid => App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null)); - var moveElement = new MoveElements(elementsToMove.ToList(), Group, this, App.Kp2a, new ActionOnFinish((success, message) => { StopMovingElements(); if (!String.IsNullOrEmpty(message)) Toast.MakeText(this, message, ToastLength.Long).Show();})); + + var moveElement = new MoveElements(elementsToMove.ToList(), Group, this, App.Kp2a, new ActionOnFinish((success, message) => { StopMovingElements(); if (!String.IsNullOrEmpty(message)) Toast.MakeText(this, message, ToastLength.Long).Show(); })); var progressTask = new ProgressTask(App.Kp2a, this, moveElement); progressTask.Run(); } - - + + protected void SetGroupTitle() { String name = Group.Name; @@ -300,31 +311,34 @@ namespace keepass2android if (!String.IsNullOrEmpty(name)) { titleText = name; - } else + } + else { titleText = GetText(Resource.String.root); } SupportActionBar.Title = titleText; - if (clickable) - { - SupportActionBar.SetHomeButtonEnabled(true); - SupportActionBar.SetDisplayHomeAsUpEnabled(true); - SupportActionBar.SetDisplayShowHomeEnabled(true); - } - + if (clickable) + { + SupportActionBar.SetHomeButtonEnabled(true); + SupportActionBar.SetDisplayHomeAsUpEnabled(true); + SupportActionBar.SetDisplayShowHomeEnabled(true); + } + } - - protected void SetGroupIcon() { - if (Group != null) { + + protected void SetGroupIcon() + { + if (Group != null) + { Drawable drawable = App.Kp2a.GetDb().DrawableFactory.GetIconDrawable(this, App.Kp2a.GetDb().KpDatabase, Group.IconId, Group.CustomIconUuid, true); - SupportActionBar.SetDisplayShowHomeEnabled(true); - //SupportActionBar.SetIcon(drawable); + SupportActionBar.SetDisplayShowHomeEnabled(true); + //SupportActionBar.SetIcon(drawable); } } - class SuggestionListener: Java.Lang.Object, SearchView.IOnSuggestionListener, Android.Support.V7.Widget.SearchView.IOnSuggestionListener + class SuggestionListener : Java.Lang.Object, SearchView.IOnSuggestionListener, Android.Support.V7.Widget.SearchView.IOnSuggestionListener { private readonly CursorAdapter _suggestionsAdapter; private readonly GroupBaseActivity _activity; @@ -343,7 +357,7 @@ namespace keepass2android var cursor = _suggestionsAdapter.Cursor; cursor.MoveToPosition(position); string entryIdAsHexString = cursor.GetString(cursor.GetColumnIndexOrThrow(SearchManager.SuggestColumnIntentDataId)); - EntryActivity.Launch(_activity, App.Kp2a.GetDb().Entries[new PwUuid(MemUtil.HexStringToByteArray(entryIdAsHexString))],-1,_activity.AppTask); + EntryActivity.Launch(_activity, App.Kp2a.GetDb().Entries[new PwUuid(MemUtil.HexStringToByteArray(entryIdAsHexString))], -1, _activity.AppTask); return true; } @@ -353,7 +367,7 @@ namespace keepass2android } } - class OnQueryTextListener: Java.Lang.Object, Android.Support.V7.Widget.SearchView.IOnQueryTextListener + class OnQueryTextListener : Java.Lang.Object, Android.Support.V7.Widget.SearchView.IOnQueryTextListener { private readonly GroupBaseActivity _activity; @@ -379,98 +393,143 @@ namespace keepass2android _activity.AppTask.ToIntent(searchIntent); _activity.StartActivityForResult(searchIntent, 0); - + return true; } } - public override bool OnCreateOptionsMenu(IMenu menu) { - - MenuInflater inflater = MenuInflater; - inflater.Inflate(Resource.Menu.group, menu); - var searchManager = (SearchManager)GetSystemService (Context.SearchService); - IMenuItem searchItem = menu.FindItem(Resource.Id.menu_search); - var view = MenuItemCompat.GetActionView(searchItem); - var searchView = view.JavaCast(); + public override bool OnCreateOptionsMenu(IMenu menu) + { - searchView.SetSearchableInfo (searchManager.GetSearchableInfo (ComponentName)); - searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); - searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); - - ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent, ActionBar.LayoutParams.MatchParent); - searchView.LayoutParameters = lparams; - - var item = menu.FindItem(Resource.Id.menu_sync); - if (item != null) - { - if (App.Kp2a.GetDb().Ioc.IsLocalFile()) - item.SetVisible(false); - else - item.SetVisible(true); - } + MenuInflater inflater = MenuInflater; + inflater.Inflate(Resource.Menu.group, menu); + var searchManager = (SearchManager)GetSystemService(Context.SearchService); + IMenuItem searchItem = menu.FindItem(Resource.Id.menu_search); + var view = MenuItemCompat.GetActionView(searchItem); + var searchView = view.JavaCast(); + + searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName)); + searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); + searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); + + ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent, + ActionBar.LayoutParams.MatchParent); + searchView.LayoutParameters = lparams; + + _syncItem = menu.FindItem(Resource.Id.menu_sync); + + + _offlineItem = menu.FindItem(Resource.Id.menu_work_offline); + _onlineItem = menu.FindItem(Resource.Id.menu_work_online); + + UpdateOfflineModeMenu(); - return base.OnCreateOptionsMenu(menu); + return base.OnCreateOptionsMenu(menu); + } + + private void UpdateOfflineModeMenu() + { + if (_syncItem != null) + { + if (App.Kp2a.GetDb().Ioc.IsLocalFile()) + _syncItem.SetVisible(false); + else + _syncItem.SetVisible(!App.Kp2a.OfflineMode); } + if (App.Kp2a.GetFileStorage(App.Kp2a.GetDb().Ioc) is IOfflineSwitchable) + { + if (_offlineItem != null) + _offlineItem.SetVisible(App.Kp2a.OfflineMode == false); + if (_onlineItem != null) + _onlineItem.SetVisible(App.Kp2a.OfflineMode); + } + else + { + if (_offlineItem != null) + _offlineItem.SetVisible(false); + if (_onlineItem != null) + _onlineItem.SetVisible(false); + + + + } + } - - public override bool OnPrepareOptionsMenu(IMenu menu) { - if ( ! base.OnPrepareOptionsMenu(menu) ) { + public override bool OnPrepareOptionsMenu(IMenu menu) + { + if (!base.OnPrepareOptionsMenu(menu)) + { return false; } Util.PrepareDonateOptionMenu(menu, this); - - + + return true; } - - public override bool OnOptionsItemSelected(IMenuItem item) { - switch ( item.ItemId ) { - case Resource.Id.menu_donate: - return Util.GotoDonateUrl(this); - case Resource.Id.menu_lock: - App.Kp2a.LockDatabase(); - return true; - case Resource.Id.menu_search: - case Resource.Id.menu_search_advanced: - OnSearchRequested(); - return true; - - case Resource.Id.menu_app_settings: - DatabaseSettingsActivity.Launch(this); - return true; - - case Resource.Id.menu_sync: - Synchronize(); - return true; - - case Resource.Id.menu_sort: - ChangeSort(); - return true; - case Android.Resource.Id.Home: - //Currently the action bar only displays the home button when we come from a previous activity. - //So we can simply Finish. See this page for information on how to do this in more general (future?) cases: - //http://developer.android.com/training/implementing-navigation/ancestral.html - AppTask.SetActivityResult(this, KeePass.ExitNormal); - Finish(); - //OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back); + public override bool OnOptionsItemSelected(IMenuItem item) + { + switch (item.ItemId) + { + case Resource.Id.menu_donate: + return Util.GotoDonateUrl(this); + case Resource.Id.menu_lock: + App.Kp2a.LockDatabase(); + return true; - return true; + case Resource.Id.menu_search: + case Resource.Id.menu_search_advanced: + OnSearchRequested(); + return true; + + case Resource.Id.menu_app_settings: + DatabaseSettingsActivity.Launch(this); + return true; + + case Resource.Id.menu_sync: + Synchronize(); + return true; + + case Resource.Id.menu_work_offline: + App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = true; + UpdateOfflineModeMenu(); + return true; + + case Resource.Id.menu_work_online: + App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = false; + UpdateOfflineModeMenu(); + Synchronize(); + return true; + + + case Resource.Id.menu_sort: + ChangeSort(); + return true; + case Android.Resource.Id.Home: + //Currently the action bar only displays the home button when we come from a previous activity. + //So we can simply Finish. See this page for information on how to do this in more general (future?) cases: + //http://developer.android.com/training/implementing-navigation/ancestral.html + AppTask.SetActivityResult(this, KeePass.ExitNormal); + Finish(); + //OverridePendingTransition(Resource.Animation.anim_enter_back, Resource.Animation.anim_leave_back); + + return true; } - + return base.OnOptionsItemSelected(item); } - public class SyncOtpAuxFile: RunnableOnFinish + public class SyncOtpAuxFile : RunnableOnFinish { private readonly IOConnectionInfo _ioc; - public SyncOtpAuxFile(IOConnectionInfo ioc) : base(null) + public SyncOtpAuxFile(IOConnectionInfo ioc) + : base(null) { _ioc = ioc; } @@ -489,11 +548,11 @@ namespace keepass2android } catch (Exception e) { - + Finish(false, e.Message); } - - + + } } @@ -507,20 +566,20 @@ namespace keepass2android if (!String.IsNullOrEmpty(message)) Toast.MakeText(this, message, ToastLength.Long).Show(); - // Tell the adapter to refresh it's list + // Tell the adapter to refresh it's list BaseAdapter adapter = (BaseAdapter)ListAdapter; adapter.NotifyDataSetChanged(); - - if (App.Kp2a.GetDb().OtpAuxFileIoc != null) + + if (App.Kp2a.GetDb().OtpAuxFileIoc != null) { var task2 = new SyncOtpAuxFile(App.Kp2a.GetDb().OtpAuxFileIoc); new ProgressTask(App.Kp2a, this, task2).Run(); } }); - + if (filestorage is CachingFileStorage) { - + task = new SynchronizeCachedDatabase(this, App.Kp2a, onFinish); } else @@ -529,8 +588,8 @@ namespace keepass2android task = new CheckDatabaseForChanges(this, App.Kp2a, onFinish); } - - + + var progressTask = new ProgressTask(App.Kp2a, this, task); progressTask.Run(); @@ -568,54 +627,69 @@ namespace keepass2android db.Dirty.Remove(Group); // Tell the adapter to refresh it's list - + BaseAdapter adapter = (BaseAdapter)ListAdapter; adapter.NotifyDataSetChanged(); - - + + }) .SetPositiveButton(Android.Resource.String.Ok, (sender, args) => ((Dialog)sender).Dismiss()) .Show(); - - - + + + } - - public class RefreshTask : OnFinish { - readonly GroupBaseActivity _act; - public RefreshTask(Handler handler, GroupBaseActivity act):base(handler) { + + public class RefreshTask : OnFinish + { + readonly GroupBaseActivity _act; + public RefreshTask(Handler handler, GroupBaseActivity act) + : base(handler) + { _act = act; } - public override void Run() { - if ( Success) { + public override void Run() + { + if (Success) + { _act.RefreshIfDirty(); - } else { + } + else + { DisplayMessage(_act); } } } - public class AfterDeleteGroup : OnFinish { + public class AfterDeleteGroup : OnFinish + { readonly GroupBaseActivity _act; - public AfterDeleteGroup(Handler handler, GroupBaseActivity act):base(handler) { + public AfterDeleteGroup(Handler handler, GroupBaseActivity act) + : base(handler) + { _act = act; } - - public override void Run() { - if ( Success) { + + public override void Run() + { + if (Success) + { _act.RefreshIfDirty(); - } else { - Handler.Post( () => { - Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show(); + } + else + { + Handler.Post(() => + { + Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show(); }); App.Kp2a.LockDatabase(false); } } - + } public bool IsBeingMoved(PwUuid uuid) @@ -638,21 +712,21 @@ namespace keepass2android public void StartMovingElements() { - + ShowInsertElementsButtons(); - BaseAdapter adapter = (BaseAdapter)ListAdapter; - adapter.NotifyDataSetChanged(); + BaseAdapter adapter = (BaseAdapter)ListAdapter; + adapter.NotifyDataSetChanged(); } public void ShowInsertElementsButtons() { - FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabCancelAddNew).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewGroup).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNewEntry).Visibility = ViewStates.Gone; + FindViewById(Resource.Id.fabAddNew).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.bottom_bar).Visibility = ViewStates.Visible; - FindViewById(Resource.Id.divider2).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.bottom_bar).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.divider2).Visibility = ViewStates.Visible; } public void StopMovingElements() @@ -660,19 +734,19 @@ namespace keepass2android try { MoveElementsTask moveElementsTask = (MoveElementsTask)AppTask; - foreach (var uuid in moveElementsTask.Uuids) - { - IStructureItem elementToMove = App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null); - if (elementToMove.ParentGroup != Group) - App.Kp2a.GetDb().Dirty.Add(elementToMove.ParentGroup); - } + foreach (var uuid in moveElementsTask.Uuids) + { + IStructureItem elementToMove = App.Kp2a.GetDb().KpDatabase.RootGroup.FindObject(uuid, true, null); + if (elementToMove.ParentGroup != Group) + App.Kp2a.GetDb().Dirty.Add(elementToMove.ParentGroup); + } } catch (Exception e) { //don't crash if adding to dirty fails but log the exception: Kp2aLog.Log(e.ToString()); } - + AppTask = new NullTask(); AppTask.SetupGroupBaseActivityButtons(this); BaseAdapter adapter = (BaseAdapter)ListAdapter; @@ -686,173 +760,173 @@ namespace keepass2android } } - public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener - { - private ActionMode _mode; - private int _statusBarColor; + public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener + { + private ActionMode _mode; + private int _statusBarColor; - public override void OnActivityCreated(Bundle savedInstanceState) - { - base.OnActivityCreated(savedInstanceState); - if (App.Kp2a.GetDb().CanWrite) - { - ListView.ChoiceMode = ChoiceMode.MultipleModal; - ListView.SetMultiChoiceModeListener(this); - ListView.ItemLongClick += delegate(object sender, AdapterView.ItemLongClickEventArgs args) - { - ListView.SetItemChecked(args.Position, true); - }; - - } + public override void OnActivityCreated(Bundle savedInstanceState) + { + base.OnActivityCreated(savedInstanceState); + if (App.Kp2a.GetDb().CanWrite) + { + ListView.ChoiceMode = ChoiceMode.MultipleModal; + ListView.SetMultiChoiceModeListener(this); + ListView.ItemLongClick += delegate(object sender, AdapterView.ItemLongClickEventArgs args) + { + ListView.SetItemChecked(args.Position, true); + }; - ListView.ItemClick += (sender, args) => ((GroupListItemView) args.View).OnClick(); - - StyleListView(); + } - } + ListView.ItemClick += (sender, args) => ((GroupListItemView)args.View).OnClick(); - protected void StyleListView() - { - ListView lv = ListView; - lv.ScrollBarStyle =ScrollbarStyles.InsideInset; - lv.TextFilterEnabled = true; + StyleListView(); + + } + + protected void StyleListView() + { + ListView lv = ListView; + lv.ScrollBarStyle = ScrollbarStyles.InsideInset; + lv.TextFilterEnabled = true; lv.Divider = null; - } + } - public bool OnActionItemClicked(ActionMode mode, IMenuItem item) - { - var listView = FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListView; - var checkedItemPositions = listView.CheckedItemPositions; + public bool OnActionItemClicked(ActionMode mode, IMenuItem item) + { + var listView = FragmentManager.FindFragmentById(Resource.Id.list_fragment).ListView; + var checkedItemPositions = listView.CheckedItemPositions; - List checkedItems = new List(); - for (int i = 0; i < checkedItemPositions.Size(); i++) - { - if (checkedItemPositions.ValueAt(i)) - { - checkedItems.Add(((PwGroupListAdapter) ListAdapter).GetItemAtPosition(checkedItemPositions.KeyAt(i))); - } - } + List checkedItems = new List(); + for (int i = 0; i < checkedItemPositions.Size(); i++) + { + if (checkedItemPositions.ValueAt(i)) + { + checkedItems.Add(((PwGroupListAdapter)ListAdapter).GetItemAtPosition(checkedItemPositions.KeyAt(i))); + } + } - //shouldn't happen, just in case... - if (!checkedItems.Any()) - { - return false; - } - - switch (item.ItemId) - { + //shouldn't happen, just in case... + if (!checkedItems.Any()) + { + return false; + } - case Resource.Id.menu_delete: - Handler handler = new Handler(); + switch (item.ItemId) + { + + case Resource.Id.menu_delete: + Handler handler = new Handler(); DeleteMultipleItems task = new DeleteMultipleItems((GroupBaseActivity)Activity, App.Kp2a.GetDb(), checkedItems, - new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity)), App.Kp2a); - task.Start(); - break; - case Resource.Id.menu_move: - var navMove = new NavigateToFolderAndLaunchMoveElementTask(checkedItems.First().ParentGroup, checkedItems.Select(i => i.Uuid).ToList(), ((GroupBaseActivity)Activity).IsSearchResult); - ((GroupBaseActivity)Activity).StartTask(navMove); - break; + new GroupBaseActivity.RefreshTask(handler, ((GroupBaseActivity)Activity)), App.Kp2a); + task.Start(); + break; + case Resource.Id.menu_move: + var navMove = new NavigateToFolderAndLaunchMoveElementTask(checkedItems.First().ParentGroup, checkedItems.Select(i => i.Uuid).ToList(), ((GroupBaseActivity)Activity).IsSearchResult); + ((GroupBaseActivity)Activity).StartTask(navMove); + break; case Resource.Id.menu_navigate: NavigateToFolder navNavigate = new NavigateToFolder(checkedItems.First().ParentGroup, true); ((GroupBaseActivity)Activity).StartTask(navNavigate); break; case Resource.Id.menu_edit: - GroupEditActivity.Launch(Activity, checkedItems.First().ParentGroup, (PwGroup) checkedItems.First()); - break; + GroupEditActivity.Launch(Activity, checkedItems.First().ParentGroup, (PwGroup)checkedItems.First()); + break; default: - return false; - + return false; - } + + } listView.ClearChoices(); ((BaseAdapter)ListAdapter).NotifyDataSetChanged(); if (_mode != null) mode.Finish(); - return true; - } + return true; + } - public bool OnCreateActionMode(ActionMode mode, IMenu menu) - { - MenuInflater inflater = Activity.MenuInflater; - inflater.Inflate(Resource.Menu.group_entriesselected, menu); - //mode.Title = "Select Items"; - Android.Util.Log.Debug("KP2A", "Create action mode" + mode); - ((PwGroupListAdapter) ListView.Adapter).InActionMode = true; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - _mode = mode; - if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) - { - _statusBarColor = Activity.Window.StatusBarColor; - Activity.Window.SetStatusBarColor(Activity.Resources.GetColor(Resource.Color.appAccentColorDark)); - } - return true; - } - - public void OnDestroyActionMode(ActionMode mode) - { - Android.Util.Log.Debug("KP2A", "Destroy action mode" + mode); + public bool OnCreateActionMode(ActionMode mode, IMenu menu) + { + MenuInflater inflater = Activity.MenuInflater; + inflater.Inflate(Resource.Menu.group_entriesselected, menu); + //mode.Title = "Select Items"; + Android.Util.Log.Debug("KP2A", "Create action mode" + mode); ((PwGroupListAdapter)ListView.Adapter).InActionMode = true; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - _mode = null; + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + _mode = mode; if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) { - Activity.Window.SetStatusBarColor( new Android.Graphics.Color(_statusBarColor)); + _statusBarColor = Activity.Window.StatusBarColor; + Activity.Window.SetStatusBarColor(Activity.Resources.GetColor(Resource.Color.appAccentColorDark)); } - } + return true; + } - public bool OnPrepareActionMode(ActionMode mode, IMenu menu) - { - Android.Util.Log.Debug("KP2A", "Prepare action mode" + mode); + public void OnDestroyActionMode(ActionMode mode) + { + Android.Util.Log.Debug("KP2A", "Destroy action mode" + mode); + ((PwGroupListAdapter)ListView.Adapter).InActionMode = true; + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + _mode = null; + if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) + { + Activity.Window.SetStatusBarColor(new Android.Graphics.Color(_statusBarColor)); + } + } + + public bool OnPrepareActionMode(ActionMode mode, IMenu menu) + { + Android.Util.Log.Debug("KP2A", "Prepare action mode" + mode); ((PwGroupListAdapter)ListView.Adapter).InActionMode = mode != null; - ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); - return true; - } + ((PwGroupListAdapter)ListView.Adapter).NotifyDataSetChanged(); + return true; + } - public void OnItemCheckedStateChanged(ActionMode mode, int position, long id, bool @checked) - { - var menuItem = mode.Menu.FindItem(Resource.Id.menu_edit); - if (menuItem != null) - { - menuItem.SetVisible(IsOnlyOneGroupChecked()); - } + public void OnItemCheckedStateChanged(ActionMode mode, int position, long id, bool @checked) + { + var menuItem = mode.Menu.FindItem(Resource.Id.menu_edit); + if (menuItem != null) + { + menuItem.SetVisible(IsOnlyOneGroupChecked()); + } menuItem = mode.Menu.FindItem(Resource.Id.menu_navigate); if (menuItem != null) { menuItem.SetVisible(((GroupBaseActivity)Activity).IsSearchResult && IsOnlyOneItemChecked()); } - } + } - private bool IsOnlyOneGroupChecked() - { - var checkedItems = ListView.CheckedItemPositions; - bool hadCheckedGroup = false; - if (checkedItems != null) - { - for (int i = 0; i < checkedItems.Size(); i++) - { - if (checkedItems.ValueAt(i)) - { - if (hadCheckedGroup) - { - return false; - } + private bool IsOnlyOneGroupChecked() + { + var checkedItems = ListView.CheckedItemPositions; + bool hadCheckedGroup = false; + if (checkedItems != null) + { + for (int i = 0; i < checkedItems.Size(); i++) + { + if (checkedItems.ValueAt(i)) + { + if (hadCheckedGroup) + { + return false; + } - if (((PwGroupListAdapter) ListAdapter).IsGroupAtPosition(checkedItems.KeyAt(i))) - { - hadCheckedGroup = true; - } - else - { - return false; - } - } - } - } - return hadCheckedGroup; - } + if (((PwGroupListAdapter)ListAdapter).IsGroupAtPosition(checkedItems.KeyAt(i))) + { + hadCheckedGroup = true; + } + else + { + return false; + } + } + } + } + return hadCheckedGroup; + } private bool IsOnlyOneItemChecked() { @@ -875,6 +949,6 @@ namespace keepass2android } return hadCheckedItem; } - } + } } diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index c3d7d7c8..a1765ca6 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -102,6 +102,8 @@ namespace keepass2android private Task _loadDbTask; + private bool _loadDbTaskOffline; //indicate if preloading was started with offline mode + private IOConnectionInfo _ioConnection; private String _keyFileOrProvider; bool _showPassword; @@ -687,6 +689,7 @@ namespace keepass2android private string mDrawerTitle; private MeasuringRelativeLayout.MeasureArgs _measureArgs; private ActivityDesign _activityDesign; + internal class MyActionBarDrawerToggle : ActionBarDrawerToggle { @@ -862,7 +865,7 @@ namespace keepass2android InitializeTogglePasswordButton(); InitializeKeyfileBrowseButton(); - InitializeQuickUnlockCheckbox(); + InitializeOptionCheckboxes(); RestoreState(savedInstanceState); @@ -1270,6 +1273,17 @@ namespace keepass2android CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock); App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked); + if (App.Kp2a.OfflineMode != _loadDbTaskOffline) + { + //keep the loading result if we loaded in online-mode (now offline) and the task is completed + if (!App.Kp2a.OfflineMode || !_loadDbTask.IsCompleted) + { + //discard the pre-loading task + _loadDbTask = null; + } + + } + //avoid password being visible while loading: _showPassword = false; MakePasswordMaskedOrVisible(); @@ -1577,6 +1591,20 @@ namespace keepass2android base.OnResume(); _activityDesign.ReapplyTheme(); + CheckBox cbOfflineMode = (CheckBox)FindViewById(Resource.Id.work_offline); + App.Kp2a.OfflineMode = cbOfflineMode.Checked = App.Kp2a.OfflineModePreference; //this won't overwrite new user settings because every change is directly saved in settings + LinearLayout offlineModeContainer = FindViewById(Resource.Id.work_offline_container); + if (App.Kp2a.GetFileStorage(_ioConnection) is IOfflineSwitchable) + { + offlineModeContainer.Visibility = ViewStates.Visible; + } + else + { + offlineModeContainer.Visibility = ViewStates.Gone; + App.Kp2a.OfflineMode = false; + } + + EditText pwd = FindViewById(Resource.Id.password_edit); pwd.PostDelayed(() => { @@ -1650,14 +1678,22 @@ namespace keepass2android { // Create task to kick off file loading while the user enters the password _loadDbTask = Task.Factory.StartNew(PreloadDbFile); + _loadDbTaskOffline = App.Kp2a.OfflineMode; } } } } - private void InitializeQuickUnlockCheckbox() { + private void InitializeOptionCheckboxes() { CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock); cbQuickUnlock.Checked = _prefs.GetBoolean(GetString(Resource.String.QuickUnlockDefaultEnabled_key), true); + + CheckBox cbOfflineMode = (CheckBox)FindViewById(Resource.Id.work_offline); + cbOfflineMode.CheckedChange += (sender, args) => + { + App.Kp2a.OfflineModePreference = App.Kp2a.OfflineMode = args.IsChecked; + }; + } private String GetKeyFile(String filename) { diff --git a/src/keepass2android/Resources/layout/password.xml b/src/keepass2android/Resources/layout/password.xml index 3f63dfa0..13946b1f 100644 --- a/src/keepass2android/Resources/layout/password.xml +++ b/src/keepass2android/Resources/layout/password.xml @@ -308,12 +308,51 @@ android:layout_width="fill_parent" android:layout_marginTop="16dp" android:layout_height="wrap_content" /> + + + + + + + + + + + + + + @@ -88,6 +90,8 @@ FileHandling_prefs_key keyboardswitch_prefs_key + OfflineMode_key + Enable_QuickUnlock_by_default QuickUnlockLength 3 diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 2a7e979c..8b854184 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -386,6 +386,11 @@ Merging changes… Yes, merge No, overwrite + + Work offline + Work online + Avoid any network traffic by using the local cache copy of the file. Changes are stored in the local cache only and will only be uploaded when switching back to online mode. + Working offline. Synchronizing cached database… Downloading remote file… @@ -453,7 +458,9 @@ You can store your database locally on your Android device or in the cloud (non-Offline version only). Keepass2Android makes the database available even if you are offline. As the database is securely encrypted with AES 256 bit encryption, nobody will be able to access your passwords except you. We recommend to select Dropbox: It\'s accessible on all your devices and even provides backups of previous file versions. Select where you want to store the database: Change location - + + If enabled, Keepass2Android stays running in the background even when the database is locked. This allows to unlock the database later with only a short part of the master password. + Master password Your database is encrypted with the password you enter here. Choose a strong password in order to keep the database safe! Tip: Make up a sentence or two and use the first letters of the words as password. Include punctuation marks. Select a master password to protect your database: diff --git a/src/keepass2android/Resources/values/styles.xml b/src/keepass2android/Resources/values/styles.xml index 6950e65b..c45c3324 100644 --- a/src/keepass2android/Resources/values/styles.xml +++ b/src/keepass2android/Resources/values/styles.xml @@ -116,7 +116,20 @@ - + + +