allow to save attachments to deliberate storages, closes #346

This commit is contained in:
Philipp Crocoll
2018-09-17 12:56:10 +02:00
parent 9c88ce213b
commit 2dc125e0ce
5 changed files with 401 additions and 204 deletions

View File

@@ -30,22 +30,61 @@ using Android.Widget;
using Android.Preferences;
using Android.Text.Method;
using System.Globalization;
using System.IO;
using System.Net;
using Android.Content.PM;
using Android.Webkit;
using Android.Graphics;
using Java.IO;
using keepass2android.EntryActivityClasses;
using KeePassLib;
using KeePassLib.Security;
using KeePassLib.Utility;
using Keepass2android.Pluginsdk;
using keepass2android.Io;
using KeePass.DataExchange;
using KeePass.Util.Spr;
using KeePassLib.Interfaces;
using KeePassLib.Serialization;
using File = Java.IO.File;
using Uri = Android.Net.Uri;
namespace keepass2android
{
public class ExportBinaryProcessManager : FileSaveProcessManager
{
private readonly string _binaryToSave;
public ExportBinaryProcessManager(int requestCode, Activity activity, string key) : base(requestCode, activity)
{
_binaryToSave = key;
}
public ExportBinaryProcessManager(int requestCode, EntryActivity activity, Bundle savedInstanceState) : base(requestCode, activity)
{
_binaryToSave = savedInstanceState.GetString("BinaryToSave", null);
}
protected override void SaveFile(IOConnectionInfo ioc)
{
var task = new EntryActivity.WriteBinaryTask(_activity, App.Kp2a, new ActionOnFinish(_activity, (success, message, activity) =>
{
if (!success)
Toast.MakeText(activity, message, ToastLength.Long).Show();
}
), ((EntryActivity)_activity).Entry.Binaries.Get(_binaryToSave), ioc);
ProgressTask pt = new ProgressTask(App.Kp2a, _activity, task);
pt.Run();
}
public override void OnSaveInstanceState(Bundle outState)
{
outState.PutString("BinaryToSave", _binaryToSave);
}
}
[Activity (Label = "@string/app_name", ConfigurationChanges=ConfigChanges.Orientation|ConfigChanges.KeyboardHidden,
Theme = "@style/MyTheme_ActionBar")]
@@ -56,7 +95,12 @@ namespace keepass2android
public const String KeyCloseAfterCreate = "close_after_create";
public const String KeyGroupFullPath = "groupfullpath_key";
public static void Launch(Activity act, PwEntry pw, int pos, AppTask appTask, ActivityFlags? flags = null)
public const int requestCodeBinaryFilename = 42376;
public const int requestCodeSelFileStorageForWriteAttachment = 42377;
public static void Launch(Activity act, PwEntry pw, int pos, AppTask appTask, ActivityFlags? flags = null)
{
Intent i = new Intent(act, typeof(EntryActivity));
@@ -84,7 +128,7 @@ namespace keepass2android
_activityDesign = new ActivityDesign(this);
}
protected PwEntry Entry;
public PwEntry Entry;
private static Typeface _passwordFont;
@@ -113,9 +157,10 @@ namespace keepass2android
private PluginActionReceiver _pluginActionReceiver;
private PluginFieldReceiver _pluginFieldReceiver;
private ActivityDesign _activityDesign;
protected void SetEntryView()
protected void SetEntryView()
{
SetContentView(Resource.Layout.entry_view);
}
@@ -316,8 +361,14 @@ namespace keepass2android
protected override void OnCreate(Bundle savedInstanceState)
{
if (savedInstanceState != null)
{
_exportBinaryProcessManager =
new ExportBinaryProcessManager(requestCodeSelFileStorageForWriteAttachment, this, savedInstanceState);
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
}
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
long usageCount = prefs.GetLong(GetString(Resource.String.UsageCount_key), 0);
@@ -527,74 +578,83 @@ namespace keepass2android
_popupMenuItems[popupKey] = new List<IPopupMenuItem>();
return _popupMenuItems[popupKey];
}
internal Uri WriteBinaryToFile(string key, bool writeToCacheDirectory)
{
ProtectedBinary pb = Entry.Binaries.Get(key);
System.Diagnostics.Debug.Assert(pb != null);
if (pb == null)
throw new ArgumentException();
internal Uri WriteBinaryToFile(string key, bool writeToCacheDirectory)
{
ProtectedBinary pb = Entry.Binaries.Get(key);
System.Diagnostics.Debug.Assert(pb != null);
if (pb == null)
throw new ArgumentException();
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key), GetString(Resource.String.BinaryDirectory_default));
if (writeToCacheDirectory)
binaryDirectory = CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir;
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key),
GetString(Resource.String.BinaryDirectory_default));
if (writeToCacheDirectory)
{
binaryDirectory = CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir;
string filepart = key;
if (writeToCacheDirectory)
{
Java.Lang.String javaFilename = new Java.Lang.String(filepart);
filepart = javaFilename.ReplaceAll("[^a-zA-Z0-9.-]", "_");
}
var targetFile = new File(binaryDirectory, filepart);
string filepart = key;
Java.Lang.String javaFilename = new Java.Lang.String(filepart);
filepart = javaFilename.ReplaceAll("[^a-zA-Z0-9.-]", "_");
File parent = targetFile.ParentFile;
var targetFile = new File(binaryDirectory, filepart);
if (parent == null || (parent.Exists() && !parent.IsDirectory))
{
Toast.MakeText(this,
Resource.String.error_invalid_path,
ToastLength.Long).Show();
return null;
}
File parent = targetFile.ParentFile;
if (!parent.Exists())
{
// Create parent directory
if (!parent.Mkdirs())
{
Toast.MakeText(this,
Resource.String.error_could_not_create_parent,
ToastLength.Long).Show();
return null;
if (parent == null || (parent.Exists() && !parent.IsDirectory))
{
Toast.MakeText(this,
Resource.String.error_invalid_path,
ToastLength.Long).Show();
return null;
}
}
}
string filename = targetFile.AbsolutePath;
Uri fileUri = Uri.FromFile(targetFile);
if (!parent.Exists())
{
// Create parent directory
if (!parent.Mkdirs())
{
Toast.MakeText(this,
Resource.String.error_could_not_create_parent,
ToastLength.Long).Show();
return null;
byte[] pbData = pb.ReadData();
try
{
System.IO.File.WriteAllBytes(filename, pbData);
}
catch (Exception exWrite)
{
Toast.MakeText(this, GetString(Resource.String.SaveAttachment_Failed, new Java.Lang.Object[] { filename })
+ exWrite.Message, ToastLength.Long).Show();
return null;
}
finally
{
MemUtil.ZeroByteArray(pbData);
}
Toast.MakeText(this, GetString(Resource.String.SaveAttachment_doneMessage, new Java.Lang.Object[] { filename }), ToastLength.Short).Show();
if (writeToCacheDirectory)
{
return Uri.Parse("content://" + AttachmentContentProvider.Authority + "/"
+ filename);
}
return fileUri;
}
}
string filename = targetFile.AbsolutePath;
byte[] pbData = pb.ReadData();
try
{
System.IO.File.WriteAllBytes(filename, pbData);
}
catch (Exception exWrite)
{
Toast.MakeText(this,
GetString(Resource.String.SaveAttachment_Failed, new Java.Lang.Object[] {filename})
+ exWrite.Message, ToastLength.Long).Show();
return null;
}
finally
{
MemUtil.ZeroByteArray(pbData);
}
Toast.MakeText(this,
GetString(Resource.String.SaveAttachment_doneMessage, new Java.Lang.Object[] {filename}),
ToastLength.Short).Show();
return Uri.Parse("content://" + AttachmentContentProvider.Authority + "/"
+ filename);
}
else
{
_exportBinaryProcessManager =
new ExportBinaryProcessManager(requestCodeSelFileStorageForWriteAttachment, this, key);
_exportBinaryProcessManager.StartProcess();
return null;
}
}
internal void OpenBinaryFile(Android.Net.Uri uri)
@@ -904,10 +964,16 @@ namespace keepass2android
_appTask.ToIntent(ret);
SetResult(KeePass.ExitRefresh, ret);
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) {
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) {
base.OnActivityResult(requestCode, resultCode, data);
if (AppTask.TryGetFromActivityResult(data, ref _appTask))
if (_exportBinaryProcessManager?.OnActivityResult(requestCode, resultCode, data) == true)
{
return;
}
if (AppTask.TryGetFromActivityResult(data, ref _appTask))
{
//make sure app task is passed to calling activity.
//the result code might be modified later.
@@ -927,7 +993,71 @@ namespace keepass2android
}
}
public override bool OnCreateOptionsMenu(IMenu menu)
public class WriteBinaryTask : RunnableOnFinish
{
private readonly IKp2aApp _app;
private readonly ProtectedBinary _data;
private IOConnectionInfo _targetIoc;
public WriteBinaryTask(Activity activity, IKp2aApp app, OnFinish onFinish, ProtectedBinary data, IOConnectionInfo targetIoc) : base(activity, onFinish)
{
_app = app;
_data = data;
_targetIoc = targetIoc;
}
public override void Run()
{
try
{
var fileStorage = _app.GetFileStorage(_targetIoc);
if (fileStorage is IOfflineSwitchable)
{
((IOfflineSwitchable)fileStorage).IsOffline = false;
}
using (var writeTransaction = fileStorage.OpenWriteTransaction(_targetIoc, _app.GetDb().KpDatabase.UseFileTransactions))
{
Stream sOut = writeTransaction.OpenFile();
byte[] byteArray = _data.ReadData();
sOut.Write(byteArray, 0, byteArray.Length);
sOut.Close();
writeTransaction.CommitWrite();
}
if (fileStorage is IOfflineSwitchable)
{
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode;
}
Finish(true);
}
catch (Exception ex)
{
Finish(false, ex.Message);
}
}
}
private ExportBinaryProcessManager _exportBinaryProcessManager;
protected override void OnSaveInstanceState(Bundle outState)
{
_exportBinaryProcessManager?.OnSaveInstanceState(outState);
base.OnSaveInstanceState(outState);
}
public override bool OnCreateOptionsMenu(IMenu menu)
{
_menu = menu;
base.OnCreateOptionsMenu(menu);

View File

@@ -13,6 +13,31 @@ using keepass2android.Io;
namespace keepass2android
{
public class ExportDbProcessManager: FileSaveProcessManager
{
private readonly FileFormatProvider _ffp;
public ExportDbProcessManager(int requestCode, Activity activity, FileFormatProvider ffp) : base(requestCode, activity)
{
_ffp = ffp;
}
protected override void SaveFile(IOConnectionInfo ioc)
{
var exportDb = new ExportDatabaseActivity.ExportDb(_activity, App.Kp2a, new ActionOnFinish(_activity, (success, message, activity) =>
{
if (!success)
Toast.MakeText(activity, message, ToastLength.Long).Show();
else
Toast.MakeText(activity, _activity.GetString(Resource.String.export_database_successful), ToastLength.Long).Show();
activity.Finish();
}
), _ffp, ioc);
ProgressTask pt = new ProgressTask(App.Kp2a, _activity, exportDb);
pt.Run();
}
}
[Activity(Label = "@string/app_name",
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden,
@@ -29,7 +54,9 @@ namespace keepass2android
private int _fileFormatIndex;
protected override void OnCreate(Android.OS.Bundle savedInstanceState)
private ExportDbProcessManager _exportDbProcessManager;
protected override void OnCreate(Android.OS.Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
@@ -37,12 +64,10 @@ namespace keepass2android
builder.SetSingleChoiceItems(Resource.Array.export_fileformat_options, _fileFormatIndex,
delegate(object sender, DialogClickEventArgs args) { _fileFormatIndex = args.Which; });
builder.SetPositiveButton(Android.Resource.String.Ok, delegate
{
Intent intent = new Intent(this, typeof(FileStorageSelectionActivity));
//intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, true);
StartActivityForResult(intent, 0);
});
{
_exportDbProcessManager = new ExportDbProcessManager(0, this, _ffp[_fileFormatIndex]);
_exportDbProcessManager.StartProcess();
});
builder.SetNegativeButton(Resource.String.cancel, delegate {
Finish();
});
@@ -53,143 +78,19 @@ namespace keepass2android
{
base.OnActivityResult(requestCode, resultCode, data);
if (resultCode == KeePass.ExitFileStorageSelectionOk)
{
string protocolId = data.GetStringExtra("protocolId");
if (protocolId == "content")
{
Util.ShowBrowseDialog(this, RequestCodeDbFilename, true, true);
}
else
{
FileSelectHelper fileSelectHelper = new FileSelectHelper(this, true, RequestCodeDbFilename)
{
DefaultExtension = _ffp[_fileFormatIndex].DefaultExtension
};
fileSelectHelper.OnOpen += (sender, ioc) =>
{
ExportTo(ioc);
};
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(
new FileStorageSetupInitiatorActivity(this, OnActivityResult, s => fileSelectHelper.PerformManualFileSelect(s)),
true,
RequestCodeDbFilename,
protocolId);
}
return;
}
if (_exportDbProcessManager?.OnActivityResult(requestCode, resultCode, data) == true)
return;
if (resultCode == Result.Ok)
{
if (requestCode == RequestCodeDbFilename)
{
if (data.Data.Scheme == "content")
{
if ((int)Android.OS.Build.VERSION.SdkInt >= 19)
{
//try to take persistable permissions
try
{
Kp2aLog.Log("TakePersistableUriPermission");
var takeFlags = data.Flags
& (ActivityFlags.GrantReadUriPermission
| ActivityFlags.GrantWriteUriPermission);
this.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
}
}
}
string filename = Util.IntentToFilename(data, this);
if (filename == null)
filename = data.DataString;
bool fileExists = data.GetBooleanExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.result_file_exists", true);
if (fileExists)
{
ExportTo(new IOConnectionInfo { Path = ConvertFilenameToIocPath(filename) });
}
else
{
var task = new CreateNewFilename(this, new ActionOnFinish(this, (success, messageOrFilename, activity) =>
{
if (!success)
{
Toast.MakeText(activity, messageOrFilename, ToastLength.Long).Show();
return;
}
ExportTo(new IOConnectionInfo { Path = ConvertFilenameToIocPath(messageOrFilename) });
}), filename);
new ProgressTask(App.Kp2a, this, task).Run();
}
return;
}
}
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
{
var ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
ExportTo(ioc);
return;
}
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
{
IOConnectionInfo ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
new FileSelectHelper(this, true, RequestCodeDbFilename)
{ DefaultExtension = _ffp[_fileFormatIndex].DefaultExtension}
.StartFileChooser(ioc.Path);
return;
}
Finish();
}
private void ExportTo(IOConnectionInfo ioc)
{
var exportDb = new ExportDb(this, App.Kp2a, new ActionOnFinish(this, (success, message, activity) =>
{
if (!success)
Toast.MakeText(activity, message, ToastLength.Long).Show();
else
Toast.MakeText(activity, GetString(Resource.String.export_database_successful), ToastLength.Long).Show();
activity.Finish();
}
), _ffp[_fileFormatIndex], ioc);
ProgressTask pt = new ProgressTask(App.Kp2a, this, exportDb);
pt.Run();
}
protected int RequestCodeDbFilename
{
get { return 0; }
}
private static string ConvertFilenameToIocPath(string filename)
{
if ((filename != null) && (filename.StartsWith("file://")))
{
filename = filename.Substring(7);
filename = Java.Net.URLDecoder.Decode(filename);
}
return filename;
}
public class ExportDb : RunnableOnFinish
{
private readonly IKp2aApp _app;

View File

@@ -0,0 +1,165 @@
using System;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using keepass2android.Io;
using KeePassLib.Serialization;
namespace keepass2android
{
public abstract class FileSaveProcessManager
{
private static string ConvertFilenameToIocPath(string filename)
{
if ((filename != null) && (filename.StartsWith("file://")))
{
filename = filename.Substring(7);
filename = Java.Net.URLDecoder.Decode(filename);
}
return filename;
}
protected readonly int _requestCode;
protected readonly Activity _activity;
public FileSaveProcessManager(int requestCode, Activity activity)
{
_requestCode = requestCode;
_activity = activity;
}
public bool OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (requestCode == _requestCode)
{
if (resultCode == KeePass.ExitFileStorageSelectionOk)
{
string protocolId = data.GetStringExtra("protocolId");
if (protocolId == "content")
{
Util.ShowBrowseDialog(_activity, _requestCode, true, true);
}
else
{
FileSelectHelper fileSelectHelper = new FileSelectHelper(_activity, true, _requestCode);
fileSelectHelper.OnOpen += (sender, ioc) =>
{
SaveFile(ioc);
};
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(
new FileStorageSetupInitiatorActivity(_activity, (i, result, arg3) => OnActivityResult(i, result, arg3), s => fileSelectHelper.PerformManualFileSelect(s)),
true,
_requestCode,
protocolId);
}
return true;
}
if (resultCode == (Result)FileStorageResults.FileUsagePrepared)
{
var ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
SaveFile(ioc);
return true;
}
if (resultCode == (Result)FileStorageResults.FileChooserPrepared)
{
IOConnectionInfo ioc = new IOConnectionInfo();
PasswordActivity.SetIoConnectionFromIntent(ioc, data);
new FileSelectHelper(_activity, true, _requestCode).StartFileChooser(ioc.Path);
return true;
}
if (resultCode == Result.Ok)
{
if (requestCode == _requestCode)
{
if (data.Data.Scheme == "content")
{
if ((int)Android.OS.Build.VERSION.SdkInt >= 19)
{
//try to take persistable permissions
try
{
Kp2aLog.Log("TakePersistableUriPermission");
var takeFlags = data.Flags
& (ActivityFlags.GrantReadUriPermission
| ActivityFlags.GrantWriteUriPermission);
_activity.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
}
catch (Exception e)
{
Kp2aLog.Log(e.ToString());
}
}
}
string filename = Util.IntentToFilename(data, _activity);
if (filename == null)
filename = data.DataString;
bool fileExists = data.GetBooleanExtra("group.pals.android.lib.ui.filechooser.FileChooserActivity.result_file_exists", true);
if (fileExists)
{
SaveFile(new IOConnectionInfo { Path = ConvertFilenameToIocPath(filename) });
}
else
{
var task = new CreateNewFilename(_activity, new ActionOnFinish(_activity, (success, messageOrFilename, activity) =>
{
if (!success)
{
Toast.MakeText(activity, messageOrFilename, ToastLength.Long).Show();
return;
}
SaveFile(new IOConnectionInfo { Path = ConvertFilenameToIocPath(messageOrFilename) });
}), filename);
new ProgressTask(App.Kp2a, _activity, task).Run();
}
return true;
}
}
Clear();
return true;
}
return false;
}
protected virtual void Clear()
{
}
protected abstract void SaveFile(IOConnectionInfo ioc);
public void StartProcess()
{
Intent intent = new Intent(_activity, typeof(FileStorageSelectionActivity));
//intent.PutExtra(FileStorageSelectionActivity.AllowThirdPartyAppSend, true);
_activity.StartActivityForResult(intent, _requestCode);
}
public virtual void OnSaveInstanceState(Bundle outState)
{
}
}
}

View File

@@ -314,7 +314,7 @@
<string name="BinaryDirectory_summary">Directory where file attachments are saved to.</string>
<string name="SaveAttachmentDialog_title">Save attachment</string>
<string name="SaveAttachmentDialog_text">Please select where to save the attachment.</string>
<string name="SaveAttachmentDialog_save">Save to SD card</string>
<string name="SaveAttachmentDialog_save">Export to file...</string>
<string name="SaveAttachmentDialog_open">Save to cache and open</string>
<string name="ShowAttachedImage">Show with internal image viewer</string>

View File

@@ -175,6 +175,7 @@
<ItemGroup>
<Compile Include="ChallengeXCKey.cs" />
<Compile Include="EntryActivityClasses\ViewImagePopupItem.cs" />
<Compile Include="FileSaveProcessManager.cs" />
<Compile Include="fileselect\FilteredCursor.cs" />
<Compile Include="ImageViewActivity.cs" />
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />