Compare commits

..

4 Commits

Author SHA1 Message Date
Philipp Crocoll
057a7e2f7a another nonet fix 2025-07-15 13:12:18 +02:00
Philipp Crocoll
cfb5098b38 add support for transactional upload 2025-07-15 12:18:45 +02:00
Philipp Crocoll
913222d7cb allow chunked uploads, closes https://github.com/PhilippC/keepass2android/issues/2777 2025-07-15 11:07:40 +02:00
Philipp Crocoll
3e6d86c206 correctly check if an item is a folder or file. closes https://github.com/PhilippC/keepass2android/issues/2589 2025-07-15 09:10:41 +02:00
24 changed files with 328 additions and 96 deletions

View File

@@ -140,6 +140,7 @@ jobs:
- name: Upload APK to GitHub Release
uses: softprops/action-gh-release@v2
if: github.ref_type == 'tag'
with:
draft: true
files: |

View File

@@ -140,6 +140,10 @@ namespace keepass2android
#endif
int WebDavChunkedUploadSize
{
get;
}
}
}

View File

@@ -15,7 +15,9 @@ namespace keepass2android.Io
{
get { return false; }
}
}
static public bool IsConfigured => !string.IsNullOrEmpty(AppKey) && !string.IsNullOrEmpty(AppSecret);
}
public partial class DropboxAppFolderFileStorage: JavaFileStorage
{
@@ -29,6 +31,7 @@ namespace keepass2android.Io
get { return false; }
}
static public bool IsConfigured => !string.IsNullOrEmpty(AppKey) && !string.IsNullOrEmpty(AppSecret);
}
}

View File

@@ -123,7 +123,7 @@ namespace keepass2android.Io
}
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
public virtual IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
{
return new JavaFileStorageWriteTransaction(IocToPath(ioc), useFileTransaction, this);
}

View File

@@ -6,10 +6,12 @@ using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Views;
using Android.Widget;
#if !NoNet && !EXCLUDE_JAVAFILESTORAGE
using Keepass2android.Javafilestorage;
#endif
using KeePassLib.Serialization;
@@ -19,9 +21,15 @@ namespace keepass2android.Io
#if !NoNet && !EXCLUDE_JAVAFILESTORAGE
public class WebDavFileStorage: JavaFileStorage
{
public WebDavFileStorage(IKp2aApp app) : base(new Keepass2android.Javafilestorage.WebDavStorage(app.CertificateErrorHandler), app)
{
}
private readonly IKp2aApp _app;
private readonly WebDavStorage baseWebdavStorage;
public WebDavFileStorage(IKp2aApp app, int chunkSize) : base(new Keepass2android.Javafilestorage.WebDavStorage(app.CertificateErrorHandler, chunkSize), app)
{
_app = app;
baseWebdavStorage = (WebDavStorage)Jfs;
}
public override IEnumerable<string> SupportedProtocols
{
@@ -75,6 +83,15 @@ namespace keepass2android.Io
}
return base.IocToPath(ioc);
}
}
public override IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
{
baseWebdavStorage.SetUploadChunkSize(_app.WebDavChunkedUploadSize);
return base.OpenWriteTransaction(ioc, useFileTransaction);
}
}
#endif
}

View File

@@ -12,21 +12,20 @@ namespace KeePass.Util
public static string GetErrorMessage(Exception e)
{
try
string errorMessage = e.Message;
if (e is Java.Lang.Exception javaException)
{
string errorMessage = e.Message;
if (e is Java.Lang.Exception javaException)
try
{
errorMessage = javaException.LocalizedMessage ?? javaException.Message ?? errorMessage;
}
finally
{
return errorMessage;
}
catch
{
return "";
}
}
return errorMessage;
}
}

View File

@@ -1,6 +1,7 @@
package keepass2android.javafilestorage;
import android.content.Context;
import java.math.BigInteger;
import android.content.Intent;
import android.net.Uri;
@@ -15,7 +16,10 @@ import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
@@ -24,6 +28,7 @@ import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@@ -44,23 +49,33 @@ import keepass2android.javafilestorage.webdav.DecoratedTrustManager;
import keepass2android.javafilestorage.webdav.PropfindXmlParser;
import keepass2android.javafilestorage.webdav.WebDavUtil;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.tls.OkHostnameVerifier;
import okio.BufferedSink;
public class WebDavStorage extends JavaFileStorageBase {
private final ICertificateErrorHandler mCertificateErrorHandler;
private Context appContext;
public WebDavStorage(ICertificateErrorHandler certificateErrorHandler)
int chunkSize;
public WebDavStorage(ICertificateErrorHandler certificateErrorHandler, int chunkSize)
{
this.chunkSize = chunkSize;
mCertificateErrorHandler = certificateErrorHandler;
}
public void setUploadChunkSize(int chunkSize)
{
this.chunkSize = chunkSize;
}
public String buildFullPath(String url, String username, String password) throws UnsupportedEncodingException {
String scheme = url.substring(0, url.indexOf("://"));
url = url.substring(scheme.length() + 3);
@@ -181,21 +196,119 @@ public class WebDavStorage extends JavaFileStorageBase {
return client;
}
public void renameOrMoveWebDavResource(String sourcePath, String destinationPath, boolean overwrite) throws Exception {
ConnectionInfo sourceCi = splitStringToConnectionInfo(sourcePath);
ConnectionInfo destinationCi = splitStringToConnectionInfo(destinationPath);
Request.Builder requestBuilder = new Request.Builder()
.url(new URL(sourceCi.URL))
.method("MOVE", null) // "MOVE" is the HTTP method
.header("Destination", destinationCi.URL); // New URI for the resource
// Add Overwrite header
if (overwrite) {
requestBuilder.header("Overwrite", "T"); // 'T' for true
} else {
requestBuilder.header("Overwrite", "F"); // 'F' for false
}
Request request = requestBuilder.build();
Response response = getClient(sourceCi).newCall(request).execute();
// Check the status code
if (response.isSuccessful()) {
// WebDAV MOVE can return 201 (Created) if a new resource was created at dest,
// or 204 (No Content) if moved to a pre-existing destination (e.g., just renamed).
// A 200 OK might also be returned by some servers, though 201/204 are more common.
}
else
{
throw new Exception("Rename/Move failed for " + sourceCi.URL + " to " + destinationCi.URL + ": " + response.code() + " " + response.message());
}
}
public static String generateRandomHexString(int length) {
SecureRandom secureRandom = new SecureRandom();
// Generate enough bytes to ensure we can get the desired number of hex characters.
// Each byte converts to two hex characters.
// For 8 hex characters, we need 4 bytes.
int numBytes = (int) Math.ceil(length / 2.0);
byte[] randomBytes = new byte[numBytes];
secureRandom.nextBytes(randomBytes);
// Convert the byte array to a hexadecimal string
// BigInteger(1, randomBytes) treats the byte array as a positive number.
// toString(16) converts it to a hexadecimal string.
String hexString = new BigInteger(1, randomBytes).toString(16);
// Pad with leading zeros if necessary (e.g., if the generated number is small)
// and then take the first 'length' characters.
// Using String.format to ensure leading zeros if the hexString is shorter.
return String.format("%0" + length + "d", new BigInteger(hexString, 16)).substring(0, length);
}
@Override
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
if (writeTransactional)
{
String randomSuffix = ".tmp." + generateRandomHexString(8);
uploadFile(path + randomSuffix, data, false);
renameOrMoveWebDavResource(path+randomSuffix, path, true);
return;
}
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
RequestBody requestBody;
if (chunkSize > 0)
{
// use chunked upload
requestBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("application/binary");
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
try (InputStream in = new ByteArrayInputStream(data)) {
byte[] buffer = new byte[chunkSize];
int read;
while ((read = in.read(buffer)) != -1) {
sink.write(buffer, 0, read);
sink.flush();
}
}
}
@Override
public long contentLength() {
return -1; // use chunked upload
}
};
}
else
{
requestBody = new MultipartBody.Builder()
.addPart(RequestBody.create(data, MediaType.parse("application/binary")))
.build();
}
Request request = new Request.Builder()
.url(new URL(ci.URL))
.put(RequestBody.create(MediaType.parse("application/binary"), data))
.put(requestBody)
.build();
//TODO consider writeTransactional
//TODO check for error
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
@@ -290,7 +403,10 @@ public class WebDavStorage extends JavaFileStorageBase {
e.sizeInBytes = -1;
}
}
e.isDirectory = r.href.endsWith("/");
e.isDirectory = r.href.endsWith("/") || okprop.IsCollection;
e.displayName = okprop.DisplayName;
if (e.displayName == null)
@@ -519,3 +635,4 @@ public class WebDavStorage extends JavaFileStorageBase {
}
}

View File

@@ -57,6 +57,8 @@ public class PropfindXmlParser
public String DisplayName;
public String LastModified;
public String ContentLength;
public boolean IsCollection;
}
public String status;
public Prop prop;
@@ -191,6 +193,8 @@ public class PropfindXmlParser
continue;
}
String name = parser.getName();
String namespace = parser.getNamespace();
android.util.Log.d("PARSE", "4name = " + name);
if (name.equals("getcontentlength"))
@@ -200,6 +204,9 @@ public class PropfindXmlParser
prop.LastModified = readText(parser);
} else if (name.equals("displayname")) {
prop.DisplayName = readText(parser);
} else if (name.equals("resourcetype") && namespace.equals(ns)) {
// We found the <d:resourcetype> tag
prop.IsCollection = readResourceType(parser);
} else {
skip(parser);
}
@@ -208,6 +215,37 @@ public class PropfindXmlParser
return prop;
}
private boolean readResourceType(XmlPullParser parser) throws IOException, XmlPullParserException {
boolean isCollection = false;
parser.require(XmlPullParser.START_TAG, ns, "resourcetype");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
String namespace = parser.getNamespace();
if (name.equals("collection") && namespace.equals(ns)) {
// We found <d:collection/>, so it's a folder
isCollection = true;
// Since <d:collection/> is usually an empty tag, just consume it.
// It might contain text if there's whitespace, so consume text then end tag.
if (parser.next() == XmlPullParser.TEXT) {
parser.nextTag(); // Move to the end tag
}
parser.require(XmlPullParser.END_TAG, ns, "collection");
} else {
// Skip any other unexpected tags within <d:resourcetype>
skip(parser);
}
}
// After reading all children of <d:resourcetype>, ensure we are at its END_TAG
parser.require(XmlPullParser.END_TAG, ns, "resourcetype");
return isCollection;
}
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
android.util.Log.d("PARSE", "skipping " + parser.getName());

View File

@@ -548,7 +548,7 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
//storageToTest = new GoogleDriveAppDataFileStorage();
/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
@Override
public boolean onValidationError(String error) {
return false;
@@ -558,12 +558,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
public boolean alwaysFailOnValidationError() {
return false;
}
});
*/
}, 64*1024);
//storageToTest = new DropboxV2Storage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
//storageToTest = new DropboxFileStorage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart);
//storageToTest = new DropboxAppFolderFileStorage(ctx,"ax0268uydp1ya57", "3s86datjhkihwyc", true);
storageToTest = new GoogleDriveFullFileStorage();
// storageToTest = new GoogleDriveFullFileStorage();
return storageToTest;

View File

@@ -31,8 +31,6 @@ namespace keepass2android
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ctx);
builder.SetTitle(ctx.GetString(Resource.String.ChangeLog_title));
List<string> changeLog = new List<string>{
BuildChangelogString(ctx, new List<int>{Resource.Array.ChangeLog_1_13}, "1.13"),
BuildChangelogString(ctx, new List<int>{Resource.Array.ChangeLog_1_12
#if !NoNet
,Resource.Array.ChangeLog_1_12_net

View File

@@ -9,7 +9,9 @@ using System.Text;
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Views;
using Android.Widget;
@@ -319,7 +321,7 @@ namespace keepass2android
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.httpcredentials, null);
if (!defaultPath.EndsWith(_schemeSeparator))
{
var webdavStorage = new Keepass2android.Javafilestorage.WebDavStorage(App.Kp2a.CertificateErrorHandler);
var webdavStorage = CreateWebdavStorage(activity);
var connInfo = webdavStorage.SplitStringToConnectionInfo(defaultPath);
dlgContents.FindViewById<EditText>(Resource.Id.http_url).Text = connInfo.Url;
dlgContents.FindViewById<EditText>(Resource.Id.http_user).Text = connInfo.Username;
@@ -339,7 +341,7 @@ namespace keepass2android
string scheme = defaultPath.Substring(0, defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal));
if (host.Contains(_schemeSeparator) == false)
host = scheme + _schemeSeparator + host;
string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(host, user,
string httpPath = CreateWebdavStorage(activity).BuildFullPath(host, user,
password);
onStartBrowse(httpPath);
});
@@ -352,8 +354,13 @@ namespace keepass2android
dialog.Show();
#endif
}
private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
#if !NoNet
private static WebDavStorage CreateWebdavStorage(Activity activity)
{
return new WebDavStorage(App.Kp2a.CertificateErrorHandler, App.Kp2a.WebDavChunkedUploadSize);
}
#endif
private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
{
#if !NoNet
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
@@ -518,7 +525,7 @@ namespace keepass2android
string scheme = defaultPath.Substring(0,defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal));
if (host.Contains(_schemeSeparator) == false)
host = scheme + _schemeSeparator + host;
string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(WebDavFileStorage.Owncloud2Webdav(host, subtype == "owncloud" ? WebDavFileStorage.owncloudPrefix : WebDavFileStorage.nextcloudPrefix), user,
string httpPath = CreateWebdavStorage(activity).BuildFullPath(WebDavFileStorage.Owncloud2Webdav(host, subtype == "owncloud" ? WebDavFileStorage.owncloudPrefix : WebDavFileStorage.nextcloudPrefix), user,
password);
onStartBrowse(httpPath);
});

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="235"
android:versionName="1.13-r1"
android:versionCode="221"
android:versionName="1.13-r0"
package="keepass2android.keepass2android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="235"
android:versionName="1.13-r1"
android:versionCode="221"
android:versionName="1.13-r0"
package="keepass2android.keepass2android_nonet"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

View File

@@ -1425,20 +1425,18 @@ namespace keepass2android
App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked);
App.Kp2a.ScreenLockWasEnabledWhenOpeningDatabase =
(((KeyguardManager)GetSystemService(Context.KeyguardService)!)!).IsDeviceSecure;
App.Kp2a.QuickUnlockBlockedWhenDeviceNotSecureWhenOpeningDatabase = PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.QuickUnlockBlockedWhenDeviceNotSecure_key), true);
if ((_loadDbFileTask != null) && (App.Kp2a.OfflineMode != _loadDbTaskOffline))
if ((_loadDbFileTask != null) && (App.Kp2a.OfflineMode != _loadDbTaskOffline))
{
if (App.Kp2a == null)
if (App.Kp2a == null)
throw new NullPointerException("App.Kp2a");
//keep the loading result if we loaded in online-mode (now offline) and the task is completed
if (!App.Kp2a.OfflineMode || !_loadDbFileTask.IsCompleted)
{
//discard the pre-loading task
_loadDbFileTask = null;
_loadDbFileTask = null;
}
}
//avoid password being visible while loading:

View File

@@ -175,7 +175,7 @@ namespace keepass2android
App.Kp2a.Lock(false);
};
if (App.Kp2a.ScreenLockWasEnabledWhenOpeningDatabase == false && App.Kp2a.QuickUnlockBlockedWhenDeviceNotSecureWhenOpeningDatabase)
if (App.Kp2a.ScreenLockWasEnabledWhenOpeningDatabase == false)
{
FindViewById(Resource.Id.QuickUnlockForm).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.QuickUnlockBlocked).Visibility = ViewStates.Visible;

View File

@@ -78,11 +78,11 @@ android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp">
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:id="@+id/QuickUnlockForm">
<TextView
@@ -94,12 +94,6 @@ android:paddingRight="16dp"
android:textSize="14sp"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:inputType="textPassword"
android:layout_width="wrap_content"
@@ -123,8 +117,8 @@ android:paddingRight="16dp"
android:src="@drawable/baseline_fingerprint_24"
android:scaleType="fitXY"
android:background="?android:selectableItemBackground" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_margin="12dip">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/sftp_host"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text="144.76.169.229"
android:hint="@string/hint_sftp_host" />
<TextView
android:id="@+id/portsep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<EditText
android:id="@+id/sftp_port"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="number"
android:text="22"
android:hint="@string/hint_sftp_port" />
</LinearLayout>
<EditText
android:id="@+id/sftp_user"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="philipp"
android:hint="@string/hint_username" />
<EditText
android:id="@+id/sftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"
android:text="l2uientTjVhvyfzNpksa"
android:hint="@string/hint_pass" />
<TextView
android:id="@+id/initial_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/initial_directory" />
<EditText
android:id="@+id/sftp_initial_dir"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="/home/philipp" />
</LinearLayout>

View File

@@ -123,7 +123,6 @@
<string name="QuickUnlockLength_default">3</string>
<string name="QuickUnlockIconHidden_key">QuickUnlockIconHidden_key</string>
<string name="QuickUnlockIconHidden16_key">QuickUnlockIconHidden16_key</string>
<string name="QuickUnlockBlockedWhenDeviceNotSecure_key">QuickUnlockBlockedWhenDeviceNotSecure_key</string>
<string name="UsageCount_key">UsageCount</string>
<string name="LastInfoVersionCode_key">LastInfoVersion</string>
@@ -210,6 +209,7 @@
<string name="ShowUnlockedNotification_key">ShowUnlockedNotification</string>
<bool name="ShowUnlockedNotification_default">true</bool>
<integer name="WebDavChunkedUploadSize_default">65536</integer>
<string name="PreloadDatabaseEnabled_key">PreloadDatabaseEnabled</string>
<bool name="PreloadDatabaseEnabled_default">true</bool>

View File

@@ -330,8 +330,6 @@
<string name="QuickUnlockIconHidden_summary">QuickUnlock unfortunately does not work without displaying a notification icon. Select this option to use a transparent icon.</string>
<string name="QuickUnlockIconHidden16_title">Hide QuickUnlock icon</string>
<string name="QuickUnlockIconHidden16_summary">QuickUnlock requires a notification to work properly. Select this option to display a notification without an icon.</string>
<string name="QuickUnlockBlockedWhenDeviceNotSecure_summary">Block entering the QuickUnlock key if the device is not secured by screen lock. This prevents shoulder surfing attacks. Only disable this option if you understand the risk. Note that QuickUnlock can still be used with biometrics.</string>
<string name="QuickUnlockBlockedWhenDeviceNotSecure_title">Block QuickUnlock when device is not secure</string>
<string name="QuickUnlockLength_title">Length of QuickUnlock key</string>
<string name="QuickUnlockLength_summary">Maximum number of characters used as QuickUnlock password.</string>
<string name="QuickUnlockHideLength_title">Hide QuickUnlock length</string>
@@ -731,6 +729,9 @@
<string name="EntryChannel_desc">Notification to simplify access to the currently selected entry.</string>
<string name="CloseDbAfterFailedAttempts">Close database after three failed biometric unlock attempts.</string>
<string name="WarnFingerprintInvalidated">Warning! Biometric authentication can be invalidated by Android, e.g. after adding a new fingerprint in your device settings. Make sure you always know how to unlock with your master password!</string>
<string name="webdav_chunked_upload_size_title">Chunk size for WebDav upload</string>
<string name="webdav_chunked_upload_size_summary">Size of chunks when uploading to WebDav servers in bytes. Use 0 to disable chunked upload.</string>
<string-array name="ChangeLog_1_13">
<item>Improved password quality estimation by considering most popular passwords.</item>

View File

@@ -45,6 +45,14 @@
android:title="@string/UseFileTransactions_title"
android:key="@string/UseFileTransactions_key" />
<EditTextPreference
android:key="WebDavChunkedUploadSize_str"
android:title="@string/webdav_chunked_upload_size_title"
android:summary="@string/webdav_chunked_upload_size_title"
android:defaultValue="@integer/WebDavChunkedUploadSize_default"
android:inputType="number"
/>
<CheckBoxPreference
android:enabled="true"
android:persistent="true"
@@ -80,5 +88,6 @@
android:defaultValue="true"
android:title="@string/CheckForDuplicateUuids_title"
android:key="@string/CheckForDuplicateUuids_key" />
</PreferenceScreen>

View File

@@ -26,14 +26,6 @@
android:defaultValue="false"
android:title="@string/QuickUnlockIconHidden_title"
android:key="@string/QuickUnlockIconHidden_key" />
<CheckBoxPreference
android:enabled="true"
android:persistent="true"
android:summary="@string/QuickUnlockBlockedWhenDeviceNotSecure_summary"
android:defaultValue="true"
android:title="@string/QuickUnlockBlockedWhenDeviceNotSecure_title"
android:key="@string/QuickUnlockBlockedWhenDeviceNotSecure_key" />
<CheckBoxPreference
android:enabled="true"
android:persistent="true"

View File

@@ -352,7 +352,6 @@ namespace keepass2android
}
public bool ScreenLockWasEnabledWhenOpeningDatabase { get; set; }
public bool QuickUnlockBlockedWhenDeviceNotSecureWhenOpeningDatabase { get; set; }
public bool QuickUnlockEnabled { get; private set; }
@@ -837,8 +836,8 @@ namespace keepass2android
new AndroidContentStorage(LocaleManager.LocalizedAppContext),
#if !EXCLUDE_JAVAFILESTORAGE
#if !NoNet
new DropboxFileStorage(LocaleManager.LocalizedAppContext, this),
new DropboxAppFolderFileStorage(LocaleManager.LocalizedAppContext, this),
DropboxFileStorage.IsConfigured ? new DropboxFileStorage(LocaleManager.LocalizedAppContext, this) : null,
DropboxAppFolderFileStorage.IsConfigured ? new DropboxAppFolderFileStorage(LocaleManager.LocalizedAppContext, this): null,
GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(LocaleManager.LocalizedAppContext)==ConnectionResult.Success ? new GoogleDriveFileStorage(LocaleManager.LocalizedAppContext, this) : null,
GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(LocaleManager.LocalizedAppContext)==ConnectionResult.Success ? new GoogleDriveAppDataFileStorage(LocaleManager.LocalizedAppContext, this) : null,
new OneDriveFileStorage(this),
@@ -847,8 +846,8 @@ namespace keepass2android
new OneDrive2AppFolderFileStorage(),
new SftpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled()),
new NetFtpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled),
new WebDavFileStorage(this),
new PCloudFileStorage(LocaleManager.LocalizedAppContext, this),
new WebDavFileStorage(this, WebDavChunkedUploadSize),
new PCloudFileStorage(LocaleManager.LocalizedAppContext, this),
new PCloudFileStorageAll(LocaleManager.LocalizedAppContext, this),
new MegaFileStorage(App.Context),
//new LegacyWebDavStorage(this),
@@ -1334,6 +1333,18 @@ namespace keepass2android
}
}
public int WebDavChunkedUploadSize
{
get
{
return int.Parse(PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext)
.GetString("WebDavChunkedUploadSize_str",
LocaleManager.LocalizedAppContext.Resources
.GetInteger(Resource.Integer.WebDavChunkedUploadSize_default).ToString()));
}
}
}
@@ -1459,8 +1470,7 @@ namespace keepass2android
{
Kp2aLog.LogUnexpectedError(e.Exception);
}
}
}
}

View File

@@ -131,15 +131,7 @@ namespace keepass2android.view
ev.FindViewById(Resource.Id.icon).Visibility = ViewStates.Visible;
ev.FindViewById(Resource.Id.check_mark).Visibility = ViewStates.Invisible;
_db = App.Kp2a.TryFindDatabaseForElement(_entry);
if (_db == null)
{
ev.FindViewById(Resource.Id.icon).Visibility = ViewStates.Gone;
_textView.TextFormatted = new SpannableString("(no data)");
_textviewDetails.Visibility = ViewStates.Gone;
_textgroupFullPath.Visibility = ViewStates.Gone;
return;
}
_db = App.Kp2a.FindDatabaseForElement(_entry);
ImageView iv = (ImageView)ev.FindViewById(Resource.Id.icon);
bool isExpired = pw.Expires && pw.ExpiryTime < DateTime.Now;
@@ -217,6 +209,11 @@ namespace keepass2android.view
UpdateTotp();
}
public void ConvertView(PwEntry pw, int pos)

View File

@@ -15,15 +15,14 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
along with Keepass2Android. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Widget;
using keepass2android;
using KeePassLib;
using System;
using Object = Java.Lang.Object;
namespace keepass2android.view
@@ -65,20 +64,7 @@ namespace keepass2android.view
_label = (TextView) gv.FindViewById(Resource.Id.group_label);
_label.TextSize = size-8;
Database db = App.Kp2a.TryFindDatabaseForElement(pw);
if (db == null)
{
gv.FindViewById(Resource.Id.group_icon_bkg).Visibility = ViewStates.Gone;
gv.FindViewById(Resource.Id.icon).Visibility = ViewStates.Gone;
gv.FindViewById(Resource.Id.check_mark).Visibility = ViewStates.Invisible;
_textview.Text = "(no data)";
_label.Text = "";
return;
}
Database db = App.Kp2a.FindDatabaseForElement(pw);
gv.FindViewById(Resource.Id.group_icon_bkg).Visibility = db.DrawableFactory.IsWhiteIconSet ? ViewStates.Visible : ViewStates.Gone;