Skip to content

Commit 65117ac

Browse files
committed
adds support for taking pictures and storing them as attachments. closes #23.
1 parent df20f34 commit 65117ac

10 files changed

Lines changed: 634 additions & 7 deletions

File tree

src/keepass2android-app/CapturePhotoActivity.cs

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

src/keepass2android-app/EntryEditActivity.cs

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -935,13 +935,86 @@ void AddBinaryOrAsk(Uri filename)
935935
AddBinary(filename, true);
936936
}
937937

938+
/// <summary>
939+
/// Variant of <see cref="AddBinaryOrAsk"/> for bytes that are already in memory
940+
/// (e.g. an in-app camera capture). Shows the same overwrite/rename dialog.
941+
/// </summary>
942+
void AddBinaryFromBytesOrAsk(string filename, byte[] bytes)
943+
{
944+
if (State.Entry.Binaries.Get(filename) != null)
945+
{
946+
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
947+
builder.SetTitle(GetString(Resource.String.AskOverwriteBinary_title));
948+
builder.SetMessage(GetString(Resource.String.AskOverwriteBinary));
949+
builder.SetPositiveButton(GetString(Resource.String.AskOverwriteBinary_yes), (dlgSender, dlgEvt) =>
950+
{
951+
AddBinaryFromBytes(filename, bytes, true);
952+
});
953+
builder.SetNegativeButton(GetString(Resource.String.AskOverwriteBinary_no), (dlgSender, dlgEvt) =>
954+
{
955+
AddBinaryFromBytes(filename, bytes, false);
956+
});
957+
builder.SetNeutralButton(GetString(Android.Resource.String.Cancel), (dlgSender, dlgEvt) => { });
958+
builder.Create().Show();
959+
}
960+
else
961+
{
962+
AddBinaryFromBytes(filename, bytes, true);
963+
}
964+
}
965+
966+
/// <summary>
967+
/// Stores <paramref name="bytes"/> directly as a <see cref="ProtectedBinary"/> attachment.
968+
/// When <paramref name="overwrite"/> is false the filename is made unique by appending a counter.
969+
/// </summary>
970+
void AddBinaryFromBytes(string filename, byte[] bytes, bool overwrite)
971+
{
972+
string strItem = filename;
973+
if (!overwrite)
974+
{
975+
string strBase = KeePassLib.Utility.UrlUtil.StripExtension(strItem);
976+
string strExt = "." + KeePassLib.Utility.UrlUtil.GetExtension(strItem);
977+
int nTry = 0;
978+
while (true)
979+
{
980+
string strNewName = strBase + nTry.ToString(System.Globalization.CultureInfo.InvariantCulture) + strExt;
981+
if (State.Entry.Binaries.Get(strNewName) == null)
982+
{
983+
strItem = strNewName;
984+
break;
985+
}
986+
++nTry;
987+
}
988+
}
989+
try
990+
{
991+
State.Entry.Binaries.Set(strItem, new KeePassLib.Security.ProtectedBinary(false, bytes));
992+
}
993+
catch (Exception exAttach)
994+
{
995+
App.Kp2a.ShowMessage(this,
996+
GetString(Resource.String.AttachFailed) + " " + Util.GetErrorMessage(exAttach),
997+
MessageSeverity.Error);
998+
}
999+
State.EntryModified = true;
1000+
PopulateBinaries();
1001+
}
1002+
9381003
protected override void OnResume()
9391004
{
9401005
if (_uriToAddOrAsk != null)
9411006
{
9421007
AddBinaryOrAsk(_uriToAddOrAsk);
9431008
_uriToAddOrAsk = null;
9441009
}
1010+
if (_cameraPhotoBytes != null)
1011+
{
1012+
var bytes = _cameraPhotoBytes;
1013+
var filename = _cameraPhotoFilename ?? "photo.jpg";
1014+
_cameraPhotoBytes = null;
1015+
_cameraPhotoFilename = null;
1016+
AddBinaryFromBytesOrAsk(filename, bytes);
1017+
}
9451018
base.OnResume();
9461019
}
9471020

@@ -1401,6 +1474,17 @@ protected override void OnActivityResult(int requestCode, Result resultCode, Int
14011474
}
14021475
_uriToAddOrAsk = uri; //we can't launch a dialog in onActivityResult, so delay this to onResume
14031476
}
1477+
else if (requestCode == Intents.RequestCodeCameraCapture)
1478+
{
1479+
// Transfer bytes via static field – Intent extras have a 1 MB IPC limit.
1480+
var photoBytes = CapturePhotoActivity.CapturedPhotoBytes;
1481+
CapturePhotoActivity.CapturedPhotoBytes = null;
1482+
if (photoBytes != null)
1483+
{
1484+
_cameraPhotoBytes = photoBytes;
1485+
_cameraPhotoFilename = data?.GetStringExtra(CapturePhotoActivity.ExtraFilename) ?? "photo.jpg";
1486+
}
1487+
}
14041488
return;
14051489
case Result.Canceled:
14061490
return;
@@ -1466,8 +1550,29 @@ void PopulateBinaries()
14661550
addBinaryButton.Enabled = !State.Entry.Binaries.Any();
14671551
addBinaryButton.Click += (sender, e) =>
14681552
{
1469-
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, true /*force OpenDocument if available, GetContent is not well support starting with Android 7 */);
1470-
1553+
// Offer both a file browser and an in-app camera (no disk/gallery write).
1554+
var items = new string[]
1555+
{
1556+
GetString(Resource.String.attach_browse_file),
1557+
GetString(Resource.String.attach_take_photo)
1558+
};
1559+
new Google.Android.Material.Dialog.MaterialAlertDialogBuilder(this)
1560+
.SetTitle(Resource.String.add_binary)
1561+
.SetItems(items, (dlgSender, dlgArgs) =>
1562+
{
1563+
if (dlgArgs.Which == 0)
1564+
{
1565+
Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false,
1566+
true /*force OpenDocument if available*/);
1567+
}
1568+
else
1569+
{
1570+
StartActivityForResult(
1571+
new Intent(this, typeof(CapturePhotoActivity)),
1572+
Intents.RequestCodeCameraCapture);
1573+
}
1574+
})
1575+
.Show();
14711576
};
14721577

14731578
binariesGroup.AddView(addBinaryButton, layoutParams);
@@ -1581,6 +1686,8 @@ ExtraEditView CreateExtraStringEditView(KeyValuePair<string, ProtectedString> pa
15811686
private string[] _additionalKeys = null;
15821687
private List<View> _editModeHiddenViews;
15831688
private Uri _uriToAddOrAsk;
1689+
private byte[]? _cameraPhotoBytes;
1690+
private string? _cameraPhotoFilename;
15841691

15851692
public string[] AdditionalKeys
15861693
{

src/keepass2android-app/Manifests/AndroidManifest_debug.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
282282
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="426.0dip" />
283283
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="360.0dip" />
284284
</application>
285+
<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
286+
<uses-permission android:name="android.permission.CAMERA" />
287+
<!-- required="false": app works without a camera; feature only shown on devices that have one -->
288+
<uses-feature android:name="android.hardware.camera" android:required="false" />
289+
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
285290
<uses-permission android:name="android.permission.VIBRATE" />
286291
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
287292
<uses-permission android:name="android.permission.INTERNET" />

src/keepass2android-app/Manifests/AndroidManifest_light.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
</intent-filter>
6767
</activity>
6868
</application>
69+
<!-- Camera permission for in-app attachment photo capture (runtime permission on API 23+) -->
70+
<uses-permission android:name="android.permission.CAMERA" />
71+
<uses-feature android:name="android.hardware.camera" android:required="false" />
72+
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
6973
<uses-permission android:name="android.permission.VIBRATE" />
7074
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
7175
<uses-permission android:name="android.permission.INTERNET" />

src/keepass2android-app/Manifests/AndroidManifest_net.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,10 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
310310
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
311311
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
312312

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

315318
<!-- Samsung Pass permission -->
316319
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />

src/keepass2android-app/Manifests/AndroidManifest_nonet.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,10 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
281281
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
282282
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
283283

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

286289
<!-- Samsung Pass permission -->
287290
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
In-app camera capture layout. Uses CameraX PreviewView so no photo is ever
4+
written to external storage or the media gallery.
5+
-->
6+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
7+
android:layout_width="match_parent"
8+
android:layout_height="match_parent"
9+
android:background="@android:color/black">
10+
11+
<!-- Live camera preview - fills the entire screen -->
12+
<androidx.camera.view.PreviewView
13+
android:id="@+id/camera_preview"
14+
android:layout_width="match_parent"
15+
android:layout_height="match_parent" />
16+
17+
<!-- Bottom control bar overlaid on the preview -->
18+
<LinearLayout
19+
android:id="@+id/camera_controls"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
android:layout_gravity="bottom"
23+
android:background="#CC000000"
24+
android:gravity="center_vertical"
25+
android:orientation="horizontal"
26+
android:padding="12dp">
27+
28+
<!-- Cancel / close -->
29+
<ImageButton
30+
android:id="@+id/btn_cancel"
31+
android:layout_width="0dp"
32+
android:layout_height="64dp"
33+
android:layout_weight="1"
34+
android:background="?attr/selectableItemBackgroundBorderless"
35+
android:contentDescription="@string/cancel"
36+
android:padding="12dp"
37+
android:src="@drawable/baseline_close_24"
38+
android:tint="@android:color/white" />
39+
40+
<!-- Shutter / capture (larger, center) -->
41+
<ImageButton
42+
android:id="@+id/btn_capture"
43+
android:layout_width="0dp"
44+
android:layout_height="80dp"
45+
android:layout_weight="1"
46+
android:background="?attr/selectableItemBackgroundBorderless"
47+
android:contentDescription="@string/capture_photo_title"
48+
android:padding="8dp"
49+
android:src="@drawable/baseline_camera_alt_24"
50+
android:tint="@android:color/white" />
51+
52+
<!-- Flip camera front/back -->
53+
<ImageButton
54+
android:id="@+id/btn_flip_camera"
55+
android:layout_width="0dp"
56+
android:layout_height="64dp"
57+
android:layout_weight="1"
58+
android:background="?attr/selectableItemBackgroundBorderless"
59+
android:contentDescription="@string/capture_flip_camera"
60+
android:padding="12dp"
61+
android:src="@drawable/baseline_sync_24"
62+
android:tint="@android:color/white" />
63+
64+
</LinearLayout>
65+
66+
</FrameLayout>

src/keepass2android-app/Resources/values/strings.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,15 @@
374374
<string name="field_value">Field value</string>
375375
<string name="protection">Protected field</string>
376376
<string name="add_binary">Add file attachment</string>
377+
<string name="attach_browse_file">Browse file…</string>
378+
<string name="attach_take_photo">Take photo…</string>
379+
<string name="capture_photo_title">Take photo</string>
380+
<string name="capture_flip_camera">Flip camera</string>
381+
<string name="camera_permission_required">Camera permission is required to take a photo.</string>
382+
<string name="capture_attachment_size_warning">Select photo size</string>
383+
<string name="capture_size_small">Small</string>
384+
<string name="capture_size_medium">Medium</string>
385+
<string name="capture_size_original">Original</string>
377386
<string name="add_extra_string">Add additional string</string>
378387
<string name="configure_totp">Configure TOTP</string>
379388
<string name="totp_secret_key">Secret key</string>

src/keepass2android-app/intents/Intents.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public class Intents
5858
public const int RequestCodeFileBrowseForCreate = 37322;
5959
public const int RequestCodeFileBrowseForBinary = 37323;
6060
public const int RequestCodeFileBrowseForKeyfile = 37324;
61+
public const int RequestCodeCameraCapture = 37325;
6162

6263
public const String ShowNotification = "keepass2android.show_notification";
6364
public const String UpdateKeyboard = "keepass2android.update_keyboard";

src/keepass2android-app/keepass2android-app.csproj

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -743,14 +743,22 @@
743743
<AndroidNativeLibrary Include="..\java\argon2\libs\x86_64\libargon2.so" Link="x86_64\libargon2.so" />
744744
</ItemGroup>
745745
<ItemGroup>
746-
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.7.0.5" />
747-
<PackageReference Include="Xamarin.AndroidX.AppCompat.AppCompatResources" Version="1.7.0.5" />
746+
<PackageReference Include="Xamarin.AndroidX.Camera.Camera2" Version="1.4.2.3" />
747+
<PackageReference Include="Xamarin.AndroidX.Camera.Core" Version="1.4.2.3" />
748+
<PackageReference Include="Xamarin.AndroidX.Camera.Lifecycle" Version="1.4.2.3" />
749+
<PackageReference Include="Xamarin.AndroidX.Camera.View" Version="1.4.2.3" />
750+
<!-- Pin lifecycle transitive dep required by CameraX 1.4.x -->
751+
<PackageReference Include="Xamarin.AndroidX.Lifecycle.LiveData.Core.Ktx" Version="2.9.2.1" />
752+
<!-- Pin SavedState.Ktx to match the SavedState 1.3.1.1 pulled in by CameraX 1.4.x (avoids duplicate ViewKt class in D8) -->
753+
<PackageReference Include="Xamarin.AndroidX.SavedState.SavedState.Ktx" Version="1.3.1.1" />
754+
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.1" />
755+
<PackageReference Include="Xamarin.AndroidX.AppCompat.AppCompatResources" Version="1.7.1.1" />
748756
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.30" />
749757
<PackageReference Include="Xamarin.AndroidX.Biometric" Version="1.1.0.27" />
750758
<PackageReference Include="Xamarin.AndroidX.CoordinatorLayout" Version="1.3.0" />
751-
<PackageReference Include="Xamarin.AndroidX.CursorAdapter" Version="1.0.0.31" />
752-
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.8.7.2" />
753-
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.8.7.2" />
759+
<PackageReference Include="Xamarin.AndroidX.CursorAdapter" Version="1.0.0.34" />
760+
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Common" Version="2.9.2.1" />
761+
<PackageReference Include="Xamarin.AndroidX.Lifecycle.Runtime" Version="2.9.2.1" />
754762
<PackageReference Include="Xamarin.AndroidX.Preference" Version="1.2.1.12" />
755763
<PackageReference Include="Xamarin.Google.Android.Material" Version="1.12.0.2" />
756764
<PackageReference Include="Xamarin.Google.Guava" Version="33.4.0.1" />

0 commit comments

Comments
 (0)