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);
}
/// <summary>
/// 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.
/// </summary>
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; }
}
}
}

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\IoUtil.cs" />
<Compile Include="Io\JavaFileStorage.cs" />
<Compile Include="Io\OfflineSwitchableFileStorage.cs" />
<Compile Include="Io\SftpFileStorage.cs" />
<Compile Include="Io\SkyDriveFileStorage.cs" />
<Compile Include="IProgressDialog.cs" />

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,8 @@ namespace keepass2android
private Task<MemoryStream> _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<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);
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<MemoryStream>(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) {

View File

@ -308,12 +308,51 @@
android:layout_width="fill_parent"
android:layout_marginTop="16dp"
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
android:id="@+id/enable_quickunlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
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
android:id="@+id/spacing"
android:layout_width="fill_parent"

View File

@ -47,6 +47,14 @@
android:icon="@drawable/ic_popup_sync"
android:title="@string/synchronize_database_menu"
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"

View File

@ -20,9 +20,11 @@
<resources>
<attr name="help_text" format="string" />
<attr name="title_text" format="string" />
<declare-styleable name="Kp2aShortHelpView">
<attr name="android:text"/>
<attr name="help_text" />
<attr name="title_text" />
</declare-styleable>
<declare-styleable name="TextWithHelp">
<attr name="android:text"/>
@ -88,6 +90,8 @@
<string name="FileHandling_prefs_key">FileHandling_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="QuickUnlockLength_key">QuickUnlockLength</string>
<string name="QuickUnlockLength_default">3</string>

View File

@ -386,6 +386,11 @@
<string name="SynchronizingDatabase">Merging changes…</string>
<string name="YesSynchronize">Yes, merge</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="DownloadingRemoteFile">Downloading remote file…</string>
@ -453,7 +458,9 @@
<string name="help_database_location">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.</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="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="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>

View File

@ -116,7 +116,20 @@
</style>
<style name="EditEntryButton">
<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">
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:layout_marginTop">12dip</item>
<item name="android:layout_marginLeft">24dip</item>

View File

@ -410,21 +410,27 @@ namespace keepass2android
}
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo, bool allowCache)
{
IFileStorage fileStorage;
if (iocInfo.IsLocalFile())
return new BuiltInFileStorage(this);
fileStorage = new BuiltInFileStorage(this);
else
{
IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo);
if (DatabaseCacheEnabled && allowCache)
{
return new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
fileStorage = new CachingFileStorage(innerFileStorage, Application.Context.CacheDir.Path, this);
}
else
{
return innerFileStorage;
fileStorage = innerFileStorage;
}
}
if (fileStorage is IOfflineSwitchable)
{
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode;
}
return fileStorage;
}
private IFileStorage GetCloudFileStorage(IOConnectionInfo iocInfo)
@ -591,13 +597,23 @@ namespace keepass2android
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)
{
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)
@ -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()
{
if (PreferenceManager.GetDefaultSharedPreferences(Application.Context)

View File

@ -334,6 +334,7 @@ namespace keepass2android
protected override void 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");
_design.ReapplyTheme();

View File

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