Support for Storage Access Framework

* EntryEditActivity: support content-URIs and extracting display names from them
* AndroidContentStorage.cs, SelectStorageLocationActivityBase.cs: support for SAF with persistable URI permissions
* FileChooser returns URI as data
* CreateDatabaseActivity.cs supports content URIs
* FileStorageSelectionActivity: doesn't show "Local file" anymore, now system file picker on KitKat
This commit is contained in:
Philipp Crocoll 2015-06-07 22:44:34 +02:00
parent b848e191fc
commit 80b6a1d287
14 changed files with 348 additions and 85 deletions

View File

@ -12,7 +12,6 @@
<AndroidResgenFile>Resources\Resource.designer.cs</AndroidResgenFile>
<AndroidResgenClass>Resource</AndroidResgenClass>
<AssemblyName>KeePassLib2Android</AssemblyName>
<AndroidUseLatestPlatformSdk />
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

View File

@ -2,14 +2,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.Provider;
using KeePassLib.Serialization;
namespace keepass2android.Io
@ -97,7 +92,7 @@ namespace keepass2android.Io
public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId)
{
Intent intent = new Intent();
activity.IocToIntent(intent, new IOConnectionInfo() { Path = protocolId + "://" });
activity.IocToIntent(intent, new IOConnectionInfo { Path = protocolId + "://" });
activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileChooserPrepared, intent);
}
@ -131,16 +126,48 @@ namespace keepass2android.Io
public string GetDisplayName(IOConnectionInfo ioc)
{
string displayName = null;
if (TryGetDisplayName(ioc, ref displayName))
return "content://" + displayName; //make sure we return the protocol in the display name for consistency, also expected e.g. by CreateDatabaseActivity
return ioc.Path;
}
private bool TryGetDisplayName(IOConnectionInfo ioc, ref string displayName)
{
var uri = Android.Net.Uri.Parse(ioc.Path);
var cursor = _ctx.ContentResolver.Query(uri, null, null, null, null, null);
try
{
if (cursor != null && cursor.MoveToFirst())
{
displayName = cursor.GetString(cursor.GetColumnIndex(OpenableColumns.DisplayName));
if (!string.IsNullOrEmpty(displayName))
{
return true;
}
}
}
finally
{
if (cursor != null)
cursor.Close();
}
return false;
}
public string CreateFilePath(string parent, string newFilename)
{
throw new NotImplementedException();
if (!parent.EndsWith("/"))
parent += "/";
return parent + newFilename;
}
public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
{
//TODO: required for OTP Aux file retrieval
throw new NotImplementedException();
}
@ -149,15 +176,49 @@ namespace keepass2android.Io
throw new NotImplementedException();
}
private static bool IsKitKatOrLater
{
get { return (int)Build.VERSION.SdkInt >= 19; }
}
public bool IsPermanentLocation(IOConnectionInfo ioc)
{
//on pre-Kitkat devices, content:// is always temporary:
return false;
if (!IsKitKatOrLater)
return false;
//try to get a persisted permission for the file
return _ctx.ContentResolver.PersistedUriPermissions.Any(p => p.Uri.ToString().Equals(ioc.Path));
}
public bool IsReadOnly(IOConnectionInfo ioc)
{
//on pre-Kitkat devices, we can't write content:// files
if (!IsKitKatOrLater)
{
Kp2aLog.Log("File is read-only because we're not on KitKat or later.");
return true;
}
//KitKat or later...
var uri = Android.Net.Uri.Parse(ioc.Path);
var cursor = _ctx.ContentResolver.Query(uri, null, null, null, null, null);
try
{
if (cursor != null && cursor.MoveToFirst())
{
int flags = cursor.GetInt(cursor.GetColumnIndex(DocumentsContract.Document.ColumnFlags));
Kp2aLog.Log("File flags: " + flags);
return (flags & (long) DocumentContractFlags.SupportsWrite) == 0;
}
}
finally
{
if (cursor != null)
cursor.Close();
}
return true;
}
}
@ -189,7 +250,8 @@ namespace keepass2android.Io
{
using (Stream outputStream = _ctx.ContentResolver.OpenOutputStream(Android.Net.Uri.Parse(_path)))
{
outputStream.Write(_memoryStream.ToArray(), 0, (int)_memoryStream.Length);
byte[] data = _memoryStream.ToArray();
outputStream.Write(data, 0, data.Length);
}

View File

@ -12,8 +12,8 @@
<FileAlignment>512</FileAlignment>
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<TargetFrameworkVersion>v5.0</TargetFrameworkVersion>
<AndroidUseLatestPlatformSdk />
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>

View File

@ -1,6 +1,7 @@
using System;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using Java.Net;
using KeePassLib.Serialization;
@ -36,6 +37,7 @@ namespace keepass2android
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
Kp2aLog.Log("base.onAR");
base.OnActivityResult(requestCode, resultCode, data);
if ((requestCode == RequestCodeFileStorageSelectionForPrimarySelect) || ((requestCode == RequestCodeFileStorageSelectionForCopyToWritableLocation)))
{
@ -52,7 +54,11 @@ namespace keepass2android
if (protocolId == "androidget")
{
ShowAndroidBrowseDialog(RequestCodeFileBrowseForOpen, false);
ShowAndroidBrowseDialog(browseRequestCode, false, false);
}
else if (protocolId == "content")
{
ShowAndroidBrowseDialog(browseRequestCode, browseRequestCode == RequestCodeFileFileBrowseForWritableLocation, true);
}
else
{
@ -93,7 +99,10 @@ namespace keepass2android
if (resultCode == Result.Ok)
{
Kp2aLog.Log("FileSelection returned "+data.DataString);
//TODO: don't try to extract filename if content URI
string filename = IntentToFilename(data);
Kp2aLog.Log("FileSelection returned filename " + filename);
if (filename != null)
{
if (filename.StartsWith("file://"))
@ -113,6 +122,24 @@ namespace keepass2android
{
if (data.Data.Scheme == "content")
{
if ((int) 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());
}
}
IocSelected(IOConnectionInfo.FromPath(data.DataString), requestCode);
}
@ -155,7 +182,7 @@ namespace keepass2android
/// <param name="protocolId"></param>
protected abstract void StartSelectFile(bool isForSave, int browseRequestCode, string protocolId);
protected abstract void ShowAndroidBrowseDialog(int requestCode, bool isForSave);
protected abstract void ShowAndroidBrowseDialog(int requestCode, bool isForSave, bool tryGetPermanentAccess);
protected abstract bool IsStorageSelectionForSave { get; }
@ -257,7 +284,6 @@ namespace keepass2android
var filestorage = _app.GetFileStorage(ioc, false);
if (!filestorage.IsPermanentLocation(ioc))
{
string message = _app.GetResourceString(UiStringKey.FileIsTemporarilyAvailable) + " " + _app.GetResourceString(UiStringKey.CopyFileRequired) + " " + _app.GetResourceString(UiStringKey.ClickOkToSelectLocation);
EventHandler<DialogClickEventArgs> onOk = (sender, args) => { MoveToWritableLocation(ioc); };
EventHandler<DialogClickEventArgs> onCancel = (sender, args) => { ReturnCancel(); };

View File

@ -328,7 +328,7 @@ namespace Kp2aUnitTests
}
}
protected override void ShowAndroidBrowseDialog(int requestCode, bool isForSave)
protected override void ShowAndroidBrowseDialog(int requestCode, bool isForSave, bool tryGetPermanentAccess)
{
_userAction = new AndroidBrowseDialogAction(requestCode, isForSave, this);
}

View File

@ -322,17 +322,25 @@ namespace keepass2android
if (resultCode == KeePass.ExitFileStorageSelectionOk)
{
string protocolId = data.GetStringExtra("protocolId");
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(new FileStorageSetupInitiatorActivity(this,
OnActivityResult,
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceiveSftpData, () => { });
else
Util.ShowFilenameDialog(this, OnCreateButton, null, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), true, RequestCodeDbFilename, protocolId);
if (protocolId == "content")
{
Util.ShowBrowseDialog(this, RequestCodeDbFilename, true, true);
}
else
{
App.Kp2a.GetFileStorage(protocolId).StartSelectFile(new FileStorageSetupInitiatorActivity(this,
OnActivityResult,
defaultPath =>
{
if (defaultPath.StartsWith("sftp://"))
Util.ShowSftpDialog(this, OnReceiveSftpData, () => { });
else
Util.ShowFilenameDialog(this, OnCreateButton, null, null, false, defaultPath, GetString(Resource.String.enter_filename_details_url),
Intents.RequestCodeFileBrowseForOpen);
}
), true, RequestCodeDbFilename, protocolId);
}
}
if (resultCode == Result.Ok)
@ -349,7 +357,31 @@ namespace keepass2android
}
if (requestCode == RequestCodeDbFilename)
{
if (data.Data.Scheme == "content")
{
if ((int)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);
@ -375,9 +407,6 @@ namespace keepass2android
new ProgressTask(App.Kp2a, this, task).Run();
}
}
}

View File

@ -21,9 +21,11 @@ using System.Linq;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Provider;
using Android.Views;
using Android.Widget;
using Android.Preferences;
using Java.IO;
using KeePassLib.Utility;
using KeePassLib;
using Android.Text;
@ -31,6 +33,8 @@ using KeePassLib.Security;
using Android.Content.PM;
using System.IO;
using System.Globalization;
using File = System.IO.File;
using Uri = Android.Net.Uri;
namespace keepass2android
{
@ -491,9 +495,50 @@ namespace keepass2android
}
void AddBinaryOrAsk(string filename)
public String GetFileName(Uri uri)
{
string strItem = UrlUtil.GetFileName(filename);
String result = null;
if (uri.Scheme.Equals("content"))
{
var cursor = ContentResolver.Query(uri, null, null, null, null);
try
{
if (cursor != null && cursor.MoveToFirst())
{
result = cursor.GetString(cursor.GetColumnIndex(OpenableColumns.DisplayName));
}
}
finally
{
if (cursor != null)
cursor.Close();
}
}
if (result == null)
{
result = uri.Path;
int cut = result.LastIndexOf('/');
if (cut != -1)
{
result = result.Substring(cut + 1);
}
cut = result.LastIndexOf('?');
if (cut != -1)
{
result = result.Substring(0, cut);
}
}
return result;
}
void AddBinaryOrAsk(Uri filename)
{
string strItem = GetFileName(filename);
if (String.IsNullOrEmpty(strItem))
strItem = "attachment.bin";
if(State.Entry.Binaries.Get(strItem) != null)
{
@ -523,10 +568,9 @@ namespace keepass2android
AddBinary(filename, true);
}
void AddBinary(string filename, bool overwrite)
void AddBinary(Uri filename, bool overwrite)
{
string strItem = UrlUtil.GetFileName(filename);
string strItem = GetFileName(filename);
if (!overwrite)
{
string strFileName = UrlUtil.StripExtension(strItem);
@ -547,10 +591,22 @@ namespace keepass2android
}
try
{
byte[] vBytes = File.ReadAllBytes(filename);
ProtectedBinary pb = new ProtectedBinary(false, vBytes);
State.Entry.Binaries.Set(strItem, pb);
byte[] vBytes = null;
try
{
//Android standard way to read the contents (content or file scheme)
vBytes = ReadFully(ContentResolver.OpenInputStream(filename));
}
catch (Exception)
{
//if standard way fails, try to read as a file
vBytes = File.ReadAllBytes(filename.Path);
}
ProtectedBinary pb = new ProtectedBinary(false, vBytes);
State.Entry.Binaries.Set(strItem, pb);
}
catch(Exception exAttach)
{
Toast.MakeText(this, GetString(Resource.String.AttachFailed)+" "+exAttach.Message, ToastLength.Long).Show();
@ -558,6 +614,19 @@ namespace keepass2android
State.EntryModified = true;
PopulateBinaries();
}
public static byte[] ReadFully(Stream input)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
return ms.ToArray();
}
}
protected override void OnSaveInstanceState(Bundle outState)
{
@ -637,20 +706,23 @@ namespace keepass2android
Reload();
break;
case Result.Ok:
if (requestCode == Intents.RequestCodeFileBrowseForBinary)
if (requestCode == Intents.RequestCodeFileBrowseForBinary)
{
Uri uri = data.Data;
if (data.Data == null)
{
string filename = Util.IntentToFilename(data, this);
if (filename != null) {
if (filename.StartsWith("file://")) {
filename = filename.Substring(7);
filename = Java.Net.URLDecoder.Decode(filename);
}
AddBinaryOrAsk(filename);
string s = Util.GetFilenameFromInternalFileChooser(data, this);
if (s == null)
{
Toast.MakeText(this, "No URI retrieved.", ToastLength.Short).Show();
return;
}
uri = Uri.Parse(s);
}
Reload();
AddBinaryOrAsk(uri);
}
Reload();
break;
@ -695,7 +767,7 @@ namespace keepass2android
addBinaryButton.Enabled = !State.Entry.Binaries.Any();
addBinaryButton.Click += (sender, e) =>
{
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false);
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, false);
};
binariesGroup.AddView(addBinaryButton,layoutParams);

View File

@ -39,11 +39,25 @@ namespace keepass2android
//show all supported protocols:
foreach (IFileStorage fs in App.Kp2a.FileStorages)
_protocolIds.AddRange(fs.SupportedProtocols);
//put file:// to the top
_protocolIds.Remove("file");
_protocolIds.Insert(0, "file");
//remove "content" (covered by androidget)
_protocolIds.Remove("content");
//special handling for local files:
if (!Util.IsKitKatOrLater)
{
//put file:// to the top
_protocolIds.Remove("file");
_protocolIds.Insert(0, "file");
//remove "content" (covered by androidget)
//On KitKat, content is handled by AndroidContentStorage taking advantage
//of persistable permissions and ACTION_OPEN/CREATE_DOCUMENT
_protocolIds.Remove("content");
}
else
{
_protocolIds.Remove("file");
}
if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false))
_protocolIds.Add("androidget");

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -426,6 +426,7 @@
<string name="filestoragename_gdrive">Google Drive</string>
<string name="filestoragename_skydrive">OneDrive</string>
<string name="filestoragename_sftp">SFTP (SSH File Transfer)</string>
<string name="filestoragename_content">System file picker</string>
<string name="filestorage_setup_title">File access initialization</string>

View File

@ -113,9 +113,9 @@ namespace keepass2android
), isForSave, browseRequestCode, protocolId);
}
protected override void ShowAndroidBrowseDialog(int requestCode, bool isForSave)
protected override void ShowAndroidBrowseDialog(int requestCode, bool isForSave, bool tryGetPermanentAccess)
{
Util.ShowBrowseDialog(this, requestCode, isForSave);
Util.ShowBrowseDialog(this, requestCode, isForSave, tryGetPermanentAccess);
}
protected override bool IsStorageSelectionForSave
@ -232,6 +232,12 @@ namespace keepass2android
// ReturnCancel();
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
Kp2aLog.Log("onAR");
base.OnActivityResult(requestCode, resultCode, data);
}
}

View File

@ -22,6 +22,7 @@ using System.IO;
using Android.App;
using Android.Content;
using Android.Database;
using Android.OS;
using Android.Preferences;
using Android.Provider;
using Android.Views;
@ -142,25 +143,52 @@ namespace keepass2android
return list.Count > 0;
}
public static void ShowBrowseDialog(Activity act, int requestCodeBrowse, bool forSaving)
/// <summary>
/// Opens a browse dialog for selecting a file.
/// </summary>
/// <param name="activity">context activity</param>
/// <param name="requestCodeBrowse">requestCode for onActivityResult</param>
/// <param name="forSaving">if true, the file location is meant for saving</param>
/// <param name="tryGetPermanentAccess">if true, the caller prefers a location that can be used permanently
/// This means that ActionOpenDocument should be used instead of ActionGetContent (for not saving), as ActionGetContent
/// is more for one-time access, but therefore allows possibly more available sources.</param>
public static void ShowBrowseDialog(Activity activity, int requestCodeBrowse, bool forSaving, bool tryGetPermanentAccess)
{
if ((!forSaving) && (IsIntentAvailable(act, Intent.ActionGetContent, "*/*", new List<string> { Intent.CategoryOpenable})))
var loadAction = (tryGetPermanentAccess && IsKitKatOrLater) ?
Intent.ActionOpenDocument : Intent.ActionGetContent;
if ((!forSaving) && (IsIntentAvailable(activity, loadAction, "*/*", new List<string> { Intent.CategoryOpenable})))
{
Intent i = new Intent(Intent.ActionGetContent);
Intent i = new Intent(loadAction);
i.SetType("*/*");
i.AddCategory(Intent.CategoryOpenable);
act.StartActivityForResult(i, requestCodeBrowse);
activity.StartActivityForResult(i, requestCodeBrowse);
}
else
{
string defaultPath = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
if ((forSaving) && (IsKitKatOrLater))
{
Intent i = new Intent(Intent.ActionCreateDocument);
i.SetType("*/*");
i.AddCategory(Intent.CategoryOpenable);
activity.StartActivityForResult(i, requestCodeBrowse);
}
else
{
string defaultPath = Android.OS.Environment.ExternalStorageDirectory.AbsolutePath;
ShowInternalLocalFileChooser(act, requestCodeBrowse, forSaving, defaultPath);
ShowInternalLocalFileChooser(activity, requestCodeBrowse, forSaving, defaultPath);
}
}
}
public static bool IsKitKatOrLater
{
get { return (int)Build.VERSION.SdkInt >= 19; }
}
private static void ShowInternalLocalFileChooser(Activity act, int requestCodeBrowse, bool forSaving, string defaultPath)
{
@ -179,18 +207,17 @@ namespace keepass2android
#endif
}
/// <summary>
/// Tries to extract the filename from the intent. Returns that filename or null if no success
/// (e.g. on content-URIs in Android KitKat+).
/// Guarantees that the file exists.
/// </summary>
public static string IntentToFilename(Intent data, Context ctx)
{
#if !EXCLUDE_FILECHOOSER
string EXTRA_RESULTS = "group.pals.android.lib.ui.filechooser.FileChooserActivity.results";
if (data.HasExtra(EXTRA_RESULTS))
{
IList uris = data.GetParcelableArrayListExtra(EXTRA_RESULTS);
Uri uri = (Uri) uris[0];
return Group.Pals.Android.Lib.UI.Filechooser.Providers.BaseFileProviderUtils.GetRealUri(ctx, uri).ToString();
}
string s = GetFilenameFromInternalFileChooser(data, ctx);
if (!String.IsNullOrEmpty(s))
return s;
#endif
try
{
Uri uri = data.Data;
@ -214,10 +241,30 @@ namespace keepass2android
String filename = data.Data.Path;
if ((String.IsNullOrEmpty(filename) || (!File.Exists(filename))))
filename = data.DataString;
return filename;
if (File.Exists(filename))
return filename;
//found no valid file
return null;
}
public static string GetFilenameFromInternalFileChooser(Intent data, Context ctx)
{
#if !EXCLUDE_FILECHOOSER
string EXTRA_RESULTS = "group.pals.android.lib.ui.filechooser.FileChooserActivity.results";
if (data.HasExtra(EXTRA_RESULTS))
{
IList uris = data.GetParcelableArrayListExtra(EXTRA_RESULTS);
Uri uri = (Uri) uris[0];
{
return Group.Pals.Android.Lib.UI.Filechooser.Providers.BaseFileProviderUtils.GetRealUri(ctx, uri).ToString();
}
}
#endif
return null;
}
public static bool HasActionBar(Activity activity)
{
//Actionbar is available since 11, but the layout has its own "pseudo actionbar" until 13
@ -336,11 +383,6 @@ namespace keepass2android
onCancel();
};
ImageButton browseButton = (ImageButton) dialog.FindViewById(Resource.Id.browse_button);
if (!showBrowseButton)
{
@ -350,7 +392,7 @@ namespace keepass2android
{
string filename = ((EditText) dialog.FindViewById(Resource.Id.file_filename)).Text;
Util.ShowBrowseDialog(activity, requestCodeBrowse, onCreate != null);
Util.ShowBrowseDialog(activity, requestCodeBrowse, onCreate != null, /*TODO should we prefer ActionOpenDocument here?*/ false);
};

View File

@ -450,6 +450,8 @@ namespace keepass2android
{
_fileStorages = new List<IFileStorage>
{
new AndroidContentStorage(Application.Context),
#if !EXCLUDE_JAVAFILESTORAGE
#if !NoNet
new DropboxFileStorage(Application.Context, this),
@ -459,8 +461,7 @@ namespace keepass2android
new SftpFileStorage(this),
#endif
#endif
new BuiltInFileStorage(this),
new AndroidContentStorage(Application.Context)
new BuiltInFileStorage(this)
};
}
return _fileStorages;

View File

@ -35,8 +35,17 @@
<Command type="BeforeBuild" command="UseManifestDebug.bat" />
</CustomCommands>
</CustomCommands>
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
<AndroidSupportedAbis>armeabi;armeabi-v7a;x86</AndroidSupportedAbis>
<EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk>
<AndroidSupportedAbis>armeabi,armeabi-v7a,x86</AndroidSupportedAbis>
<AndroidUseSharedRuntime>True</AndroidUseSharedRuntime>
<AndroidLinkSkip />
<BundleAssemblies>False</BundleAssemblies>
<AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi>
<AndroidStoreUncompressedFileExtensions />
<MandroidI18n />
<JavaMaximumHeapSize>1G</JavaMaximumHeapSize>
<JavaOptions />
<MonoDroidExtraArgs />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
@ -686,7 +695,6 @@
<ProjectReference Include="..\Kp2aBusinessLogic\Kp2aBusinessLogic.csproj">
<Project>{53A9CB7F-6553-4BC0-B56B-9410BB2E59AA}</Project>
<Name>Kp2aBusinessLogic</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\KP2AKdbLibraryBinding\KP2AKdbLibraryBinding.csproj">
<Project>{70D3844A-D9FA-4A64-B205-A84C6A822196}</Project>
@ -1090,4 +1098,7 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable\ic_notify_locked.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\ic_storage_content.png" />
</ItemGroup>
</Project>