Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
523 changes: 523 additions & 0 deletions src/keepass2android-app/CapturePhotoActivity.cs

Large diffs are not rendered by default.

111 changes: 109 additions & 2 deletions src/keepass2android-app/EntryEditActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -935,13 +935,86 @@ void AddBinaryOrAsk(Uri filename)
AddBinary(filename, true);
}

/// <summary>
/// Variant of <see cref="AddBinaryOrAsk"/> for bytes that are already in memory
/// (e.g. an in-app camera capture). Shows the same overwrite/rename dialog.
/// </summary>
void AddBinaryFromBytesOrAsk(string filename, byte[] bytes)
{
if (State.Entry.Binaries.Get(filename) != null)
{
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.SetTitle(GetString(Resource.String.AskOverwriteBinary_title));
builder.SetMessage(GetString(Resource.String.AskOverwriteBinary));
builder.SetPositiveButton(GetString(Resource.String.AskOverwriteBinary_yes), (dlgSender, dlgEvt) =>
{
AddBinaryFromBytes(filename, bytes, true);
});
builder.SetNegativeButton(GetString(Resource.String.AskOverwriteBinary_no), (dlgSender, dlgEvt) =>
{
AddBinaryFromBytes(filename, bytes, false);
});
builder.SetNeutralButton(GetString(Android.Resource.String.Cancel), (dlgSender, dlgEvt) => { });
builder.Create().Show();
}
else
{
AddBinaryFromBytes(filename, bytes, true);
}
}

/// <summary>
/// Stores <paramref name="bytes"/> directly as a <see cref="ProtectedBinary"/> attachment.
/// When <paramref name="overwrite"/> is false the filename is made unique by appending a counter.
/// </summary>
void AddBinaryFromBytes(string filename, byte[] bytes, bool overwrite)
{
string strItem = filename;
if (!overwrite)
{
string strBase = KeePassLib.Utility.UrlUtil.StripExtension(strItem);
string strExt = "." + KeePassLib.Utility.UrlUtil.GetExtension(strItem);
int nTry = 0;
while (true)
{
string strNewName = strBase + nTry.ToString(System.Globalization.CultureInfo.InvariantCulture) + strExt;
if (State.Entry.Binaries.Get(strNewName) == null)
{
strItem = strNewName;
break;
}
++nTry;
}
}
try
{
State.Entry.Binaries.Set(strItem, new KeePassLib.Security.ProtectedBinary(false, bytes));
}
catch (Exception exAttach)
{
App.Kp2a.ShowMessage(this,
GetString(Resource.String.AttachFailed) + " " + Util.GetErrorMessage(exAttach),
MessageSeverity.Error);
}
State.EntryModified = true;
PopulateBinaries();
}

protected override void OnResume()
{
if (_uriToAddOrAsk != null)
{
AddBinaryOrAsk(_uriToAddOrAsk);
_uriToAddOrAsk = null;
}
if (_cameraPhotoBytes != null)
{
var bytes = _cameraPhotoBytes;
var filename = _cameraPhotoFilename ?? "photo.jpg";
_cameraPhotoBytes = null;
_cameraPhotoFilename = null;
AddBinaryFromBytesOrAsk(filename, bytes);
}
base.OnResume();
}

Expand Down Expand Up @@ -1401,6 +1474,17 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int
}
_uriToAddOrAsk = uri; //we can't launch a dialog in onActivityResult, so delay this to onResume
}
else if (requestCode == Intents.RequestCodeCameraCapture)
{
// Transfer bytes via static field – Intent extras have a 1 MB IPC limit.
var photoBytes = CapturePhotoActivity.CapturedPhotoBytes;
CapturePhotoActivity.CapturedPhotoBytes = null;
if (photoBytes != null)
{
_cameraPhotoBytes = photoBytes;
_cameraPhotoFilename = data?.GetStringExtra(CapturePhotoActivity.ExtraFilename) ?? "photo.jpg";
}
}
return;
case Result.Canceled:
return;
Expand Down Expand Up @@ -1466,8 +1550,29 @@ void PopulateBinaries()
addBinaryButton.Enabled = !State.Entry.Binaries.Any();
addBinaryButton.Click += (sender, e) =>
{
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, true /*force OpenDocument if available, GetContent is not well support starting with Android 7 */);

// Offer both a file browser and an in-app camera (no disk/gallery write).
var items = new string[]
{
GetString(Resource.String.attach_browse_file),
GetString(Resource.String.attach_take_photo)
};
new Google.Android.Material.Dialog.MaterialAlertDialogBuilder(this)
.SetTitle(Resource.String.add_binary)
.SetItems(items, (dlgSender, dlgArgs) =>
{
if (dlgArgs.Which == 0)
{
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false,
true /*force OpenDocument if available*/);
}
else
{
StartActivityForResult(
new Intent(this, typeof(CapturePhotoActivity)),
Intents.RequestCodeCameraCapture);
}
})
.Show();
};

binariesGroup.AddView(addBinaryButton, layoutParams);
Expand Down Expand Up @@ -1581,6 +1686,8 @@ ExtraEditView CreateExtraStringEditView(KeyValuePair<string, ProtectedString> pa
private string[] _additionalKeys = null;
private List<View> _editModeHiddenViews;
private Uri _uriToAddOrAsk;
private byte[]? _cameraPhotoBytes;
private string? _cameraPhotoFilename;

public string[] AdditionalKeys
{
Expand Down
5 changes: 5 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_debug.xml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="426.0dip" />
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="360.0dip" />
</application>
<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- required="false": app works without a camera; feature only shown on devices that have one -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
4 changes: 4 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_light.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
</intent-filter>
</activity>
</application>
<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
3 changes: 3 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_net.xml
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,10 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />

<!-- Samsung Pass permission -->
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
Expand Down
3 changes: 3 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_nonet.xml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />

<!-- Samsung Pass permission -->
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
Expand Down
66 changes: 66 additions & 0 deletions src/keepass2android-app/Resources/layout/capture_photo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
In-app camera capture layout. Uses CameraX PreviewView so no photo is ever
written to external storage or the media gallery.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">

<!-- Live camera preview - fills the entire screen -->
<androidx.camera.view.PreviewView
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<!-- Bottom control bar overlaid on the preview -->
<LinearLayout
android:id="@+id/camera_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#CC000000"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp">

<!-- Cancel / close -->
<ImageButton
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/cancel"
android:padding="12dp"
android:src="@drawable/baseline_close_24"
android:tint="@android:color/white" />

<!-- Shutter / capture (larger, center) -->
<ImageButton
android:id="@+id/btn_capture"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/capture_photo_title"
android:padding="8dp"
android:src="@drawable/baseline_camera_alt_24"
android:tint="@android:color/white" />

<!-- Flip camera front/back -->
<ImageButton
android:id="@+id/btn_flip_camera"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/capture_flip_camera"
android:padding="12dp"
android:src="@drawable/baseline_sync_24"
android:tint="@android:color/white" />

</LinearLayout>

</FrameLayout>
9 changes: 9 additions & 0 deletions src/keepass2android-app/Resources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,15 @@
<string name="field_value">Field value</string>
<string name="protection">Protected field</string>
<string name="add_binary">Add file attachment</string>
<string name="attach_browse_file">Browse file…</string>
<string name="attach_take_photo">Take photo…</string>
<string name="capture_photo_title">Take photo</string>
<string name="capture_flip_camera">Flip camera</string>
<string name="camera_permission_required">Camera permission is required to take a photo.</string>
<string name="capture_attachment_size_warning">Select photo size</string>
<string name="capture_size_small">Small</string>
<string name="capture_size_medium">Medium</string>
<string name="capture_size_original">Original</string>
<string name="add_extra_string">Add additional string</string>
<string name="configure_totp">Configure TOTP</string>
<string name="totp_secret_key">Secret key</string>
Expand Down
1 change: 1 addition & 0 deletions src/keepass2android-app/intents/Intents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class Intents
public const int RequestCodeFileBrowseForCreate = 37322;
public const int RequestCodeFileBrowseForBinary = 37323;
public const int RequestCodeFileBrowseForKeyfile = 37324;
public const int RequestCodeCameraCapture = 37325;

public const String ShowNotification = "keepass2android.show_notification";
public const String UpdateKeyboard = "keepass2android.update_keyboard";
Expand Down
18 changes: 13 additions & 5 deletions src/keepass2android-app/keepass2android-app.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -743,14 +743,22 @@
<AndroidNativeLibrary Include="..\java\argon2\libs\x86_64\libargon2.so" Link="x86_64\libargon2.so" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.7.0.5" />
<PackageReference Include="Xamarin.AndroidX.AppCompat.AppCompatResources" Version="1.7.0.5" />
<PackageReference Include="Xamarin.AndroidX.Camera.Camera2" Version="1.4.2.3" />
<PackageReference Include="Xamarin.AndroidX.Camera.Core" Version="1.4.2.3" />
<PackageReference Include="Xamarin.AndroidX.Camera.Lifecycle" Version="1.4.2.3" />
<PackageReference Include="Xamarin.AndroidX.Camera.View" Version="1.4.2.3" />
<!-- Pin lifecycle transitive dep required by CameraX 1.4.x -->
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx" Version="2.9.2.1" />
<!-- Pin SavedState.Ktx to match the SavedState 1.3.1.1 pulled in by CameraX 1.4.x (avoids duplicate ViewKt class in D8) -->
<PackageReference Include="Xamarin.AndroidX.SavedState.SavedState.Ktx" Version="1.3.1.1" />
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.1" />
<PackageReference Include="Xamarin.AndroidX.AppCompat.AppCompatResources" Version="1.7.1.1" />
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.30" />
<PackageReference Include="Xamarin.AndroidX.Biometric" Version="1.1.0.27" />
<PackageReference Include="Xamarin.AndroidX.CoordinatorLayout" Version="1.3.0" />
<PackageReference Include="Xamarin.AndroidX.CursorAdapter" Version="1.0.0.31" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.8.7.2" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.2" />
<PackageReference Include="Xamarin.AndroidX.CursorAdapter" Version="1.0.0.34" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.9.2.1" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.9.2.1" />
<PackageReference Include="Xamarin.AndroidX.Preference" Version="1.2.1.12" />
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.12.0.2" />
<PackageReference Include="Xamarin.Google.Guava" Version="33.4.0.1" />
Expand Down
Loading