added offline mode

This commit is contained in:
Philipp Crocoll 2015-12-27 08:50:45 +01:00
parent d2850f5ea6
commit ec4fe32b29
14 changed files with 776 additions and 346 deletions

View File

@ -54,19 +54,21 @@ namespace keepass2android.Io
void LoadedFromRemoteInSync(IOConnectionInfo ioc); void LoadedFromRemoteInSync(IOConnectionInfo ioc);
} }
/// <summary> /// <summary>
/// Implements the IFileStorage interface as a proxy: A base storage is used as a remote storage. Local files are used to cache the /// 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. /// files on remote.
/// </summary> /// </summary>
public class CachingFileStorage: IFileStorage public class CachingFileStorage : IFileStorage, IOfflineSwitchable
{ {
protected readonly IFileStorage _cachedStorage;
protected readonly OfflineSwitchableFileStorage _cachedStorage;
private readonly ICacheSupervisor _cacheSupervisor; private readonly ICacheSupervisor _cacheSupervisor;
private readonly string _streamCacheDir; private readonly string _streamCacheDir;
public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor) public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor)
{ {
_cachedStorage = cachedStorage; _cachedStorage = new OfflineSwitchableFileStorage(cachedStorage);
_cacheSupervisor = cacheSupervisor; _cacheSupervisor = cacheSupervisor;
_streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator; _streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator;
if (!Directory.Exists(_streamCacheDir)) if (!Directory.Exists(_streamCacheDir))
@ -610,5 +612,11 @@ namespace keepass2android.Io
return File.OpenRead(CachedFilePath(ioc)); return File.OpenRead(CachedFilePath(ioc));
} }
} }
public bool IsOffline
{
get { return _cachedStorage.IsOffline; }
set { _cachedStorage.IsOffline = value; }
}
} }
} }

View File

@ -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; }
}
/// <summary>
/// Encapsulates another IFileStorage. Allows to switch to offline mode by throwing
/// an exception when trying to read or write a file.
/// </summary>
public class OfflineSwitchableFileStorage : IFileStorage, IOfflineSwitchable
{
private readonly IFileStorage _baseStorage;
public bool IsOffline { get; set; }
public OfflineSwitchableFileStorage(IFileStorage baseStorage)
{
_baseStorage = baseStorage;
}
public IEnumerable<string> 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<FileDescription> 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."; }
}
}
}

View File

@ -79,6 +79,7 @@
<Compile Include="Io\IFileStorage.cs" /> <Compile Include="Io\IFileStorage.cs" />
<Compile Include="Io\IoUtil.cs" /> <Compile Include="Io\IoUtil.cs" />
<Compile Include="Io\JavaFileStorage.cs" /> <Compile Include="Io\JavaFileStorage.cs" />
<Compile Include="Io\OfflineSwitchableFileStorage.cs" />
<Compile Include="Io\SftpFileStorage.cs" /> <Compile Include="Io\SftpFileStorage.cs" />
<Compile Include="Io\SkyDriveFileStorage.cs" /> <Compile Include="Io\SkyDriveFileStorage.cs" />
<Compile Include="IProgressDialog.cs" /> <Compile Include="IProgressDialog.cs" />

View File

@ -61,6 +61,7 @@ namespace keepass2android
DuplicateUuidsErrorAdditional, DuplicateUuidsErrorAdditional,
DeletingItems, DeletingItems,
AskDeletePermanentlyItems, AskDeletePermanentlyItems,
AskDeletePermanentlyItemsNoRecycle AskDeletePermanentlyItemsNoRecycle,
InOfflineMode
} }
} }

View File

@ -41,7 +41,8 @@ using Object = Java.Lang.Object;
namespace keepass2android namespace keepass2android
{ {
public abstract class GroupBaseActivity : LockCloseActivity { public abstract class GroupBaseActivity : LockCloseActivity
{
public const String KeyEntry = "entry"; public const String KeyEntry = "entry";
public const String KeyMode = "mode"; public const String KeyMode = "mode";
@ -168,12 +169,17 @@ namespace keepass2android
internal AppTask AppTask; internal AppTask AppTask;
private String strCachedGroupUuid = null; private String strCachedGroupUuid = null;
private IMenuItem _offlineItem;
private IMenuItem _onlineItem;
private IMenuItem _syncItem;
public String UuidGroup
public String UuidGroup { {
get { get
if (strCachedGroupUuid == null) { {
if (strCachedGroupUuid == null)
{
strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes); strCachedGroupUuid = MemUtil.ByteArrayToHexString(Group.Uuid.UuidBytes);
} }
return strCachedGroupUuid; return strCachedGroupUuid;
@ -181,7 +187,8 @@ namespace keepass2android
} }
protected override void OnResume() { protected override void OnResume()
{
base.OnResume(); base.OnResume();
_design.ReapplyTheme(); _design.ReapplyTheme();
AppTask.StartInGroupActivity(this); AppTask.StartInGroupActivity(this);
@ -198,9 +205,11 @@ namespace keepass2android
return true; return true;
} }
public void RefreshIfDirty() { public void RefreshIfDirty()
{
Database db = App.Kp2a.GetDb(); Database db = App.Kp2a.GetDb();
if ( db.Dirty.Contains(Group) ) { if (db.Dirty.Contains(Group))
{
db.Dirty.Remove(Group); db.Dirty.Remove(Group);
ListAdapter.NotifyDataSetChanged(); ListAdapter.NotifyDataSetChanged();
@ -217,7 +226,8 @@ namespace keepass2android
get { return false; } get { return false; }
} }
protected override void OnCreate(Bundle savedInstanceState) { protected override void OnCreate(Bundle savedInstanceState)
{
_design.ApplyTheme(); _design.ApplyTheme();
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
@ -226,7 +236,8 @@ namespace keepass2android
AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent); AppTask = AppTask.GetTaskInOnCreate(savedInstanceState, Intent);
// Likely the app has been killed exit the activity // Likely the app has been killed exit the activity
if ( ! App.Kp2a.GetDb().Loaded ) { if (!App.Kp2a.GetDb().Loaded)
{
Finish(); Finish();
return; return;
} }
@ -300,7 +311,8 @@ namespace keepass2android
if (!String.IsNullOrEmpty(name)) if (!String.IsNullOrEmpty(name))
{ {
titleText = name; titleText = name;
} else }
else
{ {
titleText = GetText(Resource.String.root); titleText = GetText(Resource.String.root);
} }
@ -316,8 +328,10 @@ namespace keepass2android
} }
protected void SetGroupIcon() { protected void SetGroupIcon()
if (Group != null) { {
if (Group != null)
{
Drawable drawable = App.Kp2a.GetDb().DrawableFactory.GetIconDrawable(this, App.Kp2a.GetDb().KpDatabase, Group.IconId, Group.CustomIconUuid, true); Drawable drawable = App.Kp2a.GetDb().DrawableFactory.GetIconDrawable(this, App.Kp2a.GetDb().KpDatabase, Group.IconId, Group.CustomIconUuid, true);
SupportActionBar.SetDisplayShowHomeEnabled(true); SupportActionBar.SetDisplayShowHomeEnabled(true);
//SupportActionBar.SetIcon(drawable); //SupportActionBar.SetIcon(drawable);
@ -384,7 +398,8 @@ namespace keepass2android
} }
} }
public override bool OnCreateOptionsMenu(IMenu menu) { public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater inflater = MenuInflater; MenuInflater inflater = MenuInflater;
inflater.Inflate(Resource.Menu.group, menu); inflater.Inflate(Resource.Menu.group, menu);
@ -397,28 +412,57 @@ namespace keepass2android
searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem)); searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter, this, searchItem));
searchView.SetOnQueryTextListener(new OnQueryTextListener(this)); searchView.SetOnQueryTextListener(new OnQueryTextListener(this));
ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent, ActionBar.LayoutParams.MatchParent); ActionBar.LayoutParams lparams = new ActionBar.LayoutParams(ActionBar.LayoutParams.MatchParent,
ActionBar.LayoutParams.MatchParent);
searchView.LayoutParameters = lparams; searchView.LayoutParameters = lparams;
var item = menu.FindItem(Resource.Id.menu_sync); _syncItem = menu.FindItem(Resource.Id.menu_sync);
if (item != null)
{
if (App.Kp2a.GetDb().Ioc.IsLocalFile()) _offlineItem = menu.FindItem(Resource.Id.menu_work_offline);
item.SetVisible(false); _onlineItem = menu.FindItem(Resource.Id.menu_work_online);
else
item.SetVisible(true); 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; return false;
} }
@ -428,8 +472,10 @@ namespace keepass2android
return true; return true;
} }
public override bool OnOptionsItemSelected(IMenuItem item) { public override bool OnOptionsItemSelected(IMenuItem item)
switch ( item.ItemId ) { {
switch (item.ItemId)
{
case Resource.Id.menu_donate: case Resource.Id.menu_donate:
return Util.GotoDonateUrl(this); return Util.GotoDonateUrl(this);
case Resource.Id.menu_lock: case Resource.Id.menu_lock:
@ -449,6 +495,18 @@ namespace keepass2android
Synchronize(); Synchronize();
return true; 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: case Resource.Id.menu_sort:
ChangeSort(); ChangeSort();
return true; return true;
@ -470,7 +528,8 @@ namespace keepass2android
{ {
private readonly IOConnectionInfo _ioc; private readonly IOConnectionInfo _ioc;
public SyncOtpAuxFile(IOConnectionInfo ioc) : base(null) public SyncOtpAuxFile(IOConnectionInfo ioc)
: base(null)
{ {
_ioc = ioc; _ioc = ioc;
} }
@ -582,33 +641,48 @@ namespace keepass2android
} }
public class RefreshTask : OnFinish { public class RefreshTask : OnFinish
{
readonly GroupBaseActivity _act; readonly GroupBaseActivity _act;
public RefreshTask(Handler handler, GroupBaseActivity act):base(handler) { public RefreshTask(Handler handler, GroupBaseActivity act)
: base(handler)
{
_act = act; _act = act;
} }
public override void Run() { public override void Run()
if ( Success) { {
if (Success)
{
_act.RefreshIfDirty(); _act.RefreshIfDirty();
} else { }
else
{
DisplayMessage(_act); DisplayMessage(_act);
} }
} }
} }
public class AfterDeleteGroup : OnFinish { public class AfterDeleteGroup : OnFinish
{
readonly GroupBaseActivity _act; readonly GroupBaseActivity _act;
public AfterDeleteGroup(Handler handler, GroupBaseActivity act):base(handler) { public AfterDeleteGroup(Handler handler, GroupBaseActivity act)
: base(handler)
{
_act = act; _act = act;
} }
public override void Run() { public override void Run()
if ( Success) { {
if (Success)
{
_act.RefreshIfDirty(); _act.RefreshIfDirty();
} else { }
Handler.Post( () => { else
{
Handler.Post(() =>
{
Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show(); Toast.MakeText(_act, "Unrecoverable error: " + Message, ToastLength.Long).Show();
}); });

View File

@ -102,6 +102,8 @@ namespace keepass2android
private Task<MemoryStream> _loadDbTask; private Task<MemoryStream> _loadDbTask;
private bool _loadDbTaskOffline; //indicate if preloading was started with offline mode
private IOConnectionInfo _ioConnection; private IOConnectionInfo _ioConnection;
private String _keyFileOrProvider; private String _keyFileOrProvider;
bool _showPassword; bool _showPassword;
@ -688,6 +690,7 @@ namespace keepass2android
private MeasuringRelativeLayout.MeasureArgs _measureArgs; private MeasuringRelativeLayout.MeasureArgs _measureArgs;
private ActivityDesign _activityDesign; private ActivityDesign _activityDesign;
internal class MyActionBarDrawerToggle : ActionBarDrawerToggle internal class MyActionBarDrawerToggle : ActionBarDrawerToggle
{ {
PasswordActivity owner; PasswordActivity owner;
@ -862,7 +865,7 @@ namespace keepass2android
InitializeTogglePasswordButton(); InitializeTogglePasswordButton();
InitializeKeyfileBrowseButton(); InitializeKeyfileBrowseButton();
InitializeQuickUnlockCheckbox(); InitializeOptionCheckboxes();
RestoreState(savedInstanceState); RestoreState(savedInstanceState);
@ -1270,6 +1273,17 @@ namespace keepass2android
CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock); CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock);
App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked); 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: //avoid password being visible while loading:
_showPassword = false; _showPassword = false;
MakePasswordMaskedOrVisible(); MakePasswordMaskedOrVisible();
@ -1577,6 +1591,20 @@ namespace keepass2android
base.OnResume(); base.OnResume();
_activityDesign.ReapplyTheme(); _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<LinearLayout>(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<EditText>(Resource.Id.password_edit); EditText pwd = FindViewById<EditText>(Resource.Id.password_edit);
pwd.PostDelayed(() => pwd.PostDelayed(() =>
{ {
@ -1650,14 +1678,22 @@ namespace keepass2android
{ {
// Create task to kick off file loading while the user enters the password // Create task to kick off file loading while the user enters the password
_loadDbTask = Task.Factory.StartNew<MemoryStream>(PreloadDbFile); _loadDbTask = Task.Factory.StartNew<MemoryStream>(PreloadDbFile);
_loadDbTaskOffline = App.Kp2a.OfflineMode;
} }
} }
} }
} }
private void InitializeQuickUnlockCheckbox() { private void InitializeOptionCheckboxes() {
CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock); CheckBox cbQuickUnlock = (CheckBox)FindViewById(Resource.Id.enable_quickunlock);
cbQuickUnlock.Checked = _prefs.GetBoolean(GetString(Resource.String.QuickUnlockDefaultEnabled_key), true); 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) { private String GetKeyFile(String filename) {

View File

@ -308,12 +308,51 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/enable_quickunlock_container"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<CheckBox <CheckBox
android:id="@+id/enable_quickunlock" android:id="@+id/enable_quickunlock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/enable_quickunlock" /> android:text="@string/enable_quickunlock" />
<keepass2android.views.Kp2aShortHelpView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance_Help_Dense"
android:textColor="@color/light_gray"
app:help_text="@string/help_quickunlock"
app:title_text="@string/enable_quickunlock"
android:text="@string/help_quickunlock"
android:background="?android:attr/selectableItemBackground"/>
</LinearLayout>
<LinearLayout
android:id="@+id/work_offline_container"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/work_offline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/UseOfflineMode" />
<keepass2android.views.Kp2aShortHelpView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance_Help_Dense"
android:textColor="@color/light_gray"
app:help_text="@string/UseOfflineMode_Info"
app:title_text="@string/UseOfflineMode"
android:text="@string/UseOfflineMode_Info"
android:background="?android:attr/selectableItemBackground"/>
</LinearLayout>
<View <View
android:id="@+id/spacing" android:id="@+id/spacing"
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -47,6 +47,14 @@
android:icon="@drawable/ic_popup_sync" android:icon="@drawable/ic_popup_sync"
android:title="@string/synchronize_database_menu" android:title="@string/synchronize_database_menu"
app:showAsAction="never" app:showAsAction="never"
/>
<item android:id="@+id/menu_work_offline"
android:title="@string/UseOfflineMode"
app:showAsAction="never"
/>
<item android:id="@+id/menu_work_online"
android:title="@string/UseOnlineMode"
app:showAsAction="never"
/> />
<item android:id="@+id/menu_sort" <item android:id="@+id/menu_sort"

View File

@ -20,9 +20,11 @@
<resources> <resources>
<attr name="help_text" format="string" /> <attr name="help_text" format="string" />
<attr name="title_text" format="string" />
<declare-styleable name="Kp2aShortHelpView"> <declare-styleable name="Kp2aShortHelpView">
<attr name="android:text"/> <attr name="android:text"/>
<attr name="help_text" /> <attr name="help_text" />
<attr name="title_text" />
</declare-styleable> </declare-styleable>
<declare-styleable name="TextWithHelp"> <declare-styleable name="TextWithHelp">
<attr name="android:text"/> <attr name="android:text"/>
@ -88,6 +90,8 @@
<string name="FileHandling_prefs_key">FileHandling_prefs_key</string> <string name="FileHandling_prefs_key">FileHandling_prefs_key</string>
<string name="keyboardswitch_prefs_key">keyboardswitch_prefs_key</string> <string name="keyboardswitch_prefs_key">keyboardswitch_prefs_key</string>
<string name="OfflineMode_key">OfflineMode_key</string>
<string name="QuickUnlockDefaultEnabled_key">Enable_QuickUnlock_by_default</string> <string name="QuickUnlockDefaultEnabled_key">Enable_QuickUnlock_by_default</string>
<string name="QuickUnlockLength_key">QuickUnlockLength</string> <string name="QuickUnlockLength_key">QuickUnlockLength</string>
<string name="QuickUnlockLength_default">3</string> <string name="QuickUnlockLength_default">3</string>

View File

@ -387,6 +387,11 @@
<string name="YesSynchronize">Yes, merge</string> <string name="YesSynchronize">Yes, merge</string>
<string name="NoOverwrite">No, overwrite</string> <string name="NoOverwrite">No, overwrite</string>
<string name="UseOfflineMode">Work offline</string>
<string name="UseOnlineMode">Work online</string>
<string name="UseOfflineMode_Info">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.</string>
<string name="InOfflineMode">Working offline.</string>
<string name="SynchronizingCachedDatabase">Synchronizing cached database…</string> <string name="SynchronizingCachedDatabase">Synchronizing cached database…</string>
<string name="DownloadingRemoteFile">Downloading remote file…</string> <string name="DownloadingRemoteFile">Downloading remote file…</string>
<string name="UploadingFile">Uploading file…</string> <string name="UploadingFile">Uploading file…</string>
@ -454,6 +459,8 @@
<string name="hint_database_location">Select where you want to store the database:</string> <string name="hint_database_location">Select where you want to store the database:</string>
<string name="button_change_location">Change location</string> <string name="button_change_location">Change location</string>
<string name="help_quickunlock">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.</string>
<string name="master_password">Master password</string> <string name="master_password">Master password</string>
<string name="help_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.</string> <string name="help_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.</string>
<string name="hint_master_password">Select a master password to protect your database:</string> <string name="hint_master_password">Select a master password to protect your database:</string>

View File

@ -116,6 +116,19 @@
</style> </style>
<style name="TextAppearance_Help_Dense">
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:textSize">18sp</item>
<item name="android:paddingLeft">6sp</item>
<item name="android:paddingRight">6sp</item>
<item name="android:paddingBottom">0sp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:layout_marginRight">4dip</item>
<item name="android:layout_marginLeft">4dip</item>
</style>
<style name="EditEntryButton"> <style name="EditEntryButton">
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item> <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:layout_marginTop">12dip</item> <item name="android:layout_marginTop">12dip</item>

View File

@ -410,21 +410,27 @@ namespace keepass2android
} }
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo, bool allowCache) public IFileStorage GetFileStorage(IOConnectionInfo iocInfo, bool allowCache)
{ {
IFileStorage fileStorage;
if (iocInfo.IsLocalFile()) if (iocInfo.IsLocalFile())
return new BuiltInFileStorage(this); fileStorage = new BuiltInFileStorage(this);
else else
{ {
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
if (DatabaseCacheEnabled && allowCache) if (DatabaseCacheEnabled && allowCache)
{ {
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this); fileStorage = new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
} }
else else
{ {
return innerFileStorage; fileStorage = innerFileStorage;
} }
} }
if (fileStorage is IOfflineSwitchable)
{
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode;
}
return fileStorage;
} }
private IFileStorage GetCloudFileStorage(IOConnectionInfo iocInfo) private IFileStorage GetCloudFileStorage(IOConnectionInfo iocInfo)
@ -591,13 +597,23 @@ namespace keepass2android
public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e) public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e)
{ {
ShowToast(Application.Context.GetString(Resource.String.CouldNotSaveToRemote, e.Message)); var errorMessage = GetErrorMessageForFileStorageException(e);
ShowToast(Application.Context.GetString(Resource.String.CouldNotSaveToRemote, errorMessage));
}
private string GetErrorMessageForFileStorageException(Exception e)
{
string errorMessage = e.Message;
if (e is OfflineModeException)
errorMessage = GetResourceString(UiStringKey.InOfflineMode);
return errorMessage;
} }
public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex) public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
{ {
ShowToast(Application.Context.GetString(Resource.String.CouldNotLoadFromRemote, ex.Message)); var errorMessage = GetErrorMessageForFileStorageException(ex);
ShowToast(Application.Context.GetString(Resource.String.CouldNotLoadFromRemote, errorMessage));
} }
public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc) public void UpdatedCachedFileOnLoad(IOConnectionInfo ioc)
@ -667,6 +683,31 @@ namespace keepass2android
} }
} }
public bool OfflineModePreference
{
get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
return prefs.GetBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), false);
}
set
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
ISharedPreferencesEditor edit = prefs.Edit();
edit.PutBoolean(Application.Context.GetString(Resource.String.OfflineMode_key), value);
edit.Commit();
}
}
/// <summary>
/// true if the app is used in offline mode
/// </summary>
public bool OfflineMode
{
get; set;
}
public void OnScreenOff() public void OnScreenOff()
{ {
if (PreferenceManager.GetDefaultSharedPreferences(Application.Context) if (PreferenceManager.GetDefaultSharedPreferences(Application.Context)

View File

@ -334,6 +334,7 @@ namespace keepass2android
protected override void OnResume() protected override void OnResume()
{ {
base.OnResume(); base.OnResume();
App.Kp2a.OfflineMode = false; //no matter what the preferences are, file selection or db creation is performed offline. PasswordActivity might set this to true.
Kp2aLog.Log("FileSelect.OnResume"); Kp2aLog.Log("FileSelect.OnResume");
_design.ReapplyTheme(); _design.ReapplyTheme();

View File

@ -50,6 +50,8 @@ namespace keepass2android.views
} }
} }
public string TitleText { get; set; }
private void UpdateView() private void UpdateView()
{ {
if (!String.IsNullOrEmpty(_helpText)) if (!String.IsNullOrEmpty(_helpText))
@ -72,8 +74,11 @@ namespace keepass2android.views
MovementMethod = LinkMovementMethod.Instance; MovementMethod = LinkMovementMethod.Instance;
Click += (sender, args) => Click += (sender, args) =>
{ {
string title = Context.GetString(AppNames.AppNameResource);
if (!string.IsNullOrEmpty(TitleText))
title = TitleText;
new AlertDialog.Builder(Context) new AlertDialog.Builder(Context)
.SetTitle(Context.GetString(AppNames.AppNameResource)) .SetTitle(title)
.SetMessage(_helpText) .SetMessage(_helpText)
.SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) => { }) .SetPositiveButton(Android.Resource.String.Ok, (o, eventArgs) => { })
.Show(); .Show();
@ -91,6 +96,7 @@ namespace keepass2android.views
TypedArray a = Context.ObtainStyledAttributes( TypedArray a = Context.ObtainStyledAttributes(
attrs, attrs,
Resource.Styleable.Kp2aShortHelpView); Resource.Styleable.Kp2aShortHelpView);
TitleText = a.GetString(Resource.Styleable.Kp2aShortHelpView_title_text);
HelpText = a.GetString(Resource.Styleable.Kp2aShortHelpView_help_text); HelpText = a.GetString(Resource.Styleable.Kp2aShortHelpView_help_text);
} }