diff --git a/src/PluginHostTest/App.cs b/src/PluginHostTest/App.cs
index 614158a7..a816d1bb 100644
--- a/src/PluginHostTest/App.cs
+++ b/src/PluginHostTest/App.cs
@@ -1,17 +1,74 @@
+using System;
using KeePassLib;
+using KeePassLib.Collections;
using KeePassLib.Keys;
+using KeePassLib.Security;
using KeePassLib.Serialization;
namespace keepass2android
{
+ ///
+ /// Represents the strings which are output from a PwEntry.
+ ///
+ /// In contrast to the original PwEntry, this means that placeholders are replaced. Also, plugins may modify
+ /// or add fields.
+ public class PwEntryOutput
+ {
+ private readonly PwEntry _entry;
+ private readonly PwDatabase _db;
+ private readonly ProtectedStringDictionary _outputStrings = new ProtectedStringDictionary();
+
+ ///
+ /// Constructs the PwEntryOutput by replacing the placeholders
+ ///
+ public PwEntryOutput(PwEntry entry, PwDatabase db)
+ {
+ _entry = entry;
+ _db = db;
+
+ foreach (var pair in entry.Strings)
+ {
+ _outputStrings.Set(pair.Key, new ProtectedString(entry.Strings.Get(pair.Key).IsProtected, GetStringAndReplacePlaceholders(pair.Key)));
+ }
+ }
+
+ string GetStringAndReplacePlaceholders(string key)
+ {
+ String value = Entry.Strings.ReadSafe(key);
+ value = SprEngine.Compile(value, new SprContext(Entry, _db, SprCompileFlags.All));
+ return value;
+ }
+
+
+ ///
+ /// Returns the ID of the entry
+ ///
+ public PwUuid Uuid
+ {
+ get { return Entry.Uuid; }
+ }
+
+ ///
+ /// The output strings for the represented entry
+ ///
+ public ProtectedStringDictionary OutputStrings { get { return _outputStrings; } }
+
+ public PwEntry Entry
+ {
+ get { return _entry; }
+ }
+ }
public class App
{
+
public class Kp2A
{
private static Db _mDb;
public class Db
{
+ public PwEntryOutput LastOpenedEntry { get; set; }
+
public void SetEntry(PwEntry e)
{
KpDatabase = new PwDatabase();
diff --git a/src/PluginHostTest/CopyToClipboardService.cs b/src/PluginHostTest/CopyToClipboardService.cs
index de30b26e..f9a345ee 100644
--- a/src/PluginHostTest/CopyToClipboardService.cs
+++ b/src/PluginHostTest/CopyToClipboardService.cs
@@ -1,14 +1,35 @@
+using System;
+using Android.App;
using Android.Content;
+using Android.OS;
+using Android.Runtime;
using Android.Widget;
using KeePassLib.Security;
namespace keepass2android
{
- internal class CopyToClipboardService
+ [Service]
+ public class CopyToClipboardService: Service
{
+ public CopyToClipboardService()
+ {
+
+ }
+
+ public CopyToClipboardService(IntPtr javaReference, JniHandleOwnership transfer)
+ : base(javaReference, transfer)
+ {
+ }
+
+
public static void CopyValueToClipboardWithTimeout(Context ctx, string text)
{
Toast.MakeText(ctx, text, ToastLength.Short).Show();
}
+
+ public override IBinder OnBind(Intent intent)
+ {
+ return null;
+ }
}
}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivity.cs b/src/PluginHostTest/EntryActivity.cs
index 5eff70f3..59ed0af6 100644
--- a/src/PluginHostTest/EntryActivity.cs
+++ b/src/PluginHostTest/EntryActivity.cs
@@ -113,6 +113,7 @@ namespace keepass2android
}
_activity.AddPluginAction(pluginPackage,
intent.GetStringExtra(Strings.ExtraFieldId),
+ intent.GetStringExtra(Strings.ExtraActionId),
intent.GetStringExtra(Strings.ExtraActionDisplayText),
intent.GetIntExtra(Strings.ExtraActionIconResId, -1),
intent.GetBundleExtra(Strings.ExtraActionData));
@@ -156,6 +157,7 @@ namespace keepass2android
private void SetPluginField(string key, string value, bool isProtected)
{
+ //update or add the string view:
IStringView existingField;
if (_stringViews.TryGetValue(key, out existingField))
{
@@ -168,13 +170,47 @@ namespace keepass2android
extraGroup.AddView(view.View);
}
+ //update the Entry output in the App database and notify the CopyToClipboard service
+ App.Kp2A.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value));
+ Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService));
+ Intent.SetAction(Intents.UpdateKeyboard);
+ updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString());
+ StartService(updateKeyboardIntent);
+
+ //notify plugins
+ NotifyPluginsOnModification(Strings.PrefixString+key);
}
- private void AddPluginAction(string pluginPackage, string fieldId, string displayText, int iconId, Bundle bundleExtra)
+ private void AddPluginAction(string pluginPackage, string fieldId, string popupItemId, string displayText, int iconId, Bundle bundleExtra)
{
if (fieldId != null)
{
- _popupMenuItems[fieldId].Add(new PluginPopupMenuItem(this, pluginPackage, fieldId, displayText, iconId, bundleExtra));
+ try
+ {
+ //create a new popup item for the plugin action:
+ var newPopup = new PluginPopupMenuItem(this, pluginPackage, fieldId, popupItemId, displayText, iconId, bundleExtra);
+ //see if we already have a popup item for this field with the same item id
+ var popupsForField = _popupMenuItems[fieldId];
+ var popupItemPos = popupsForField.FindIndex(0,
+ item =>
+ (item is PluginPopupMenuItem) &&
+ ((PluginPopupMenuItem)item).PopupItemId == popupItemId);
+
+ //replace existing or add
+ if (popupItemPos >= 0)
+ {
+ popupsForField[popupItemPos] = newPopup;
+ }
+ else
+ {
+ popupsForField.Add(newPopup);
+ }
+ }
+ catch (Exception e)
+ {
+ Kp2aLog.Log(e.ToString());
+ }
+
}
else
{
@@ -185,6 +221,7 @@ namespace keepass2android
i.SetPackage(pluginPackage);
i.PutExtra(Strings.ExtraActionData, bundleExtra);
i.PutExtra(Strings.ExtraSender, PackageName);
+ PluginHost.AddEntryToIntent(i, App.Kp2A.GetDb().LastOpenedEntry);
var menuOption = new PluginMenuOption()
{
@@ -407,6 +444,8 @@ namespace keepass2android
SetupEditButtons();
+ App.Kp2A.GetDb().LastOpenedEntry = new PwEntryOutput(Entry, App.Kp2A.GetDb().KpDatabase);
+
RegisterReceiver(new PluginActionReceiver(this), new IntentFilter(Strings.ActionAddEntryAction));
RegisterReceiver(new PluginFieldReceiver(this), new IntentFilter(Strings.ActionSetEntryField));
@@ -419,7 +458,22 @@ namespace keepass2android
Intent i = new Intent(Strings.ActionOpenEntry);
i.PutExtra(Strings.ExtraSender, PackageName);
- PluginHost.AddEntryToIntent(i, Entry);
+ AddEntryToIntent(i);
+
+
+ foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry))
+ {
+ i.SetPackage(plugin);
+ SendBroadcast(i);
+ }
+ }
+ private void NotifyPluginsOnModification(string fieldId)
+ {
+ Intent i = new Intent(Strings.ActionEntryOutputModified);
+ i.PutExtra(Strings.ExtraSender, PackageName);
+ i.PutExtra(Strings.ExtraFieldId, fieldId);
+ AddEntryToIntent(i);
+
foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry))
{
@@ -842,5 +896,10 @@ namespace keepass2android
{
Toast.MakeText(this, "opening file TODO", ToastLength.Short).Show();
}
+
+ public void AddEntryToIntent(Intent intent)
+ {
+ PluginHost.AddEntryToIntent(intent, App.Kp2A.GetDb().LastOpenedEntry);
+ }
}
}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/CopyToClipboardPopupMenuIcon.cs b/src/PluginHostTest/EntryActivityClasses/CopyToClipboardPopupMenuIcon.cs
new file mode 100644
index 00000000..a1a4207d
--- /dev/null
+++ b/src/PluginHostTest/EntryActivityClasses/CopyToClipboardPopupMenuIcon.cs
@@ -0,0 +1,41 @@
+using Android.Content;
+using Android.Graphics.Drawables;
+using PluginHostTest;
+
+namespace keepass2android
+{
+ ///
+ /// Reperesents the popup menu item in EntryActivity to copy a string to clipboard
+ ///
+ class CopyToClipboardPopupMenuIcon : IPopupMenuItem
+ {
+ private readonly Context _context;
+ private readonly IStringView _stringView;
+
+ public CopyToClipboardPopupMenuIcon(Context context, IStringView stringView)
+ {
+ _context = context;
+ _stringView = stringView;
+
+ }
+
+ public Drawable Icon
+ {
+ get
+ {
+ return _context.Resources.GetDrawable(Resource.Drawable.ic_menu_copy_holo_light);
+ }
+ }
+ public string Text
+ {
+ //TODO localize
+ get { return "Copy to clipboard"; }
+ }
+
+
+ public void HandleClick()
+ {
+ CopyToClipboardService.CopyValueToClipboardWithTimeout(_context, _stringView.Text);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/GotoUrlMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/GotoUrlMenuItem.cs
new file mode 100644
index 00000000..deacae1e
--- /dev/null
+++ b/src/PluginHostTest/EntryActivityClasses/GotoUrlMenuItem.cs
@@ -0,0 +1,34 @@
+using Android.Graphics.Drawables;
+using PluginHostTest;
+
+namespace keepass2android
+{
+ ///
+ /// Reperesents the popup menu item in EntryActivity to go to the URL in the field
+ ///
+ class GotoUrlMenuItem : IPopupMenuItem
+ {
+ private readonly EntryActivity _ctx;
+
+ public GotoUrlMenuItem(EntryActivity ctx)
+ {
+ _ctx = ctx;
+ }
+
+ public Drawable Icon
+ {
+ get { return _ctx.Resources.GetDrawable(Android.Resource.Drawable.IcMenuUpload); }
+ }
+
+ public string Text
+ {
+ get { return _ctx.Resources.GetString(Resource.String.menu_url); }
+ }
+
+ public void HandleClick()
+ {
+ //TODO
+ _ctx.GotoUrl();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs
index c9a54f65..9cb8c149 100644
--- a/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs
+++ b/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs
@@ -1,11 +1,12 @@
using System;
-using Android.Content;
using Android.Graphics.Drawables;
using KeePassLib;
-using PluginHostTest;
namespace keepass2android
{
+ ///
+ /// Interface for popup menu items in EntryActivity
+ ///
internal interface IPopupMenuItem
{
Drawable Icon { get; }
@@ -13,100 +14,4 @@ namespace keepass2android
void HandleClick();
}
-
- class GotoUrlMenuItem : IPopupMenuItem
- {
- private readonly EntryActivity _ctx;
-
- public GotoUrlMenuItem(EntryActivity ctx)
- {
- _ctx = ctx;
- }
-
- public Drawable Icon
- {
- get { return _ctx.Resources.GetDrawable(Android.Resource.Drawable.IcMenuUpload); }
- }
-
- public string Text
- {
- get { return _ctx.Resources.GetString(Resource.String.menu_url); }
- }
-
- public void HandleClick()
- {
- //TODO
- _ctx.GotoUrl();
- }
- }
-
- class ToggleVisibilityPopupMenuItem : IPopupMenuItem
- {
- private readonly EntryActivity _activity;
-
-
- public ToggleVisibilityPopupMenuItem(EntryActivity activity)
- {
- _activity = activity;
-
- }
-
- public Drawable Icon
- {
- get
- {
- //return new TextDrawable("\uF06E", _activity);
- return _activity.Resources.GetDrawable(Resource.Drawable.ic_action_eye_open);
-
- }
- }
- public string Text
- {
- get
- {
- return _activity.Resources.GetString(
- _activity._showPassword ?
- Resource.String.menu_hide_password
- : Resource.String.show_password);
- }
- }
-
-
- public void HandleClick()
- {
- _activity.ToggleVisibility();
- }
- }
-
- class CopyToClipboardPopupMenuIcon : IPopupMenuItem
- {
- private readonly Context _context;
- private readonly IStringView _stringView;
-
- public CopyToClipboardPopupMenuIcon(Context context, IStringView stringView)
- {
- _context = context;
- _stringView = stringView;
-
- }
-
- public Drawable Icon
- {
- get
- {
- return _context.Resources.GetDrawable(Resource.Drawable.ic_menu_copy_holo_light);
- }
- }
- public string Text
- {
- //TODO localize
- get { return "Copy to clipboard"; }
- }
-
-
- public void HandleClick()
- {
- CopyToClipboardService.CopyValueToClipboardWithTimeout(_context, _stringView.Text);
- }
- }
}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs b/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs
index 1315e2fb..38c07867 100644
--- a/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs
+++ b/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs
@@ -3,6 +3,9 @@ using PluginHostTest;
namespace keepass2android
{
+ ///
+ /// Represents the popup menu item in EntryActivity to open the associated attachment
+ ///
internal class OpenBinaryPopupItem : IPopupMenuItem
{
private readonly string _key;
diff --git a/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs
index 792f5bdd..779f02ad 100644
--- a/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs
+++ b/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs
@@ -5,20 +5,25 @@ using Keepass2android.Pluginsdk;
namespace keepass2android
{
+ ///
+ /// Represents a popup menu item in EntryActivity which was added by a plugin. The click will therefore broadcast to the plugin.
+ ///
class PluginPopupMenuItem : IPopupMenuItem
{
- private readonly Context _ctx;
+ private readonly EntryActivity _activity;
private readonly string _pluginPackage;
private readonly string _fieldId;
+ private readonly string _popupItemId;
private readonly string _displayText;
private readonly int _iconId;
private readonly Bundle _bundleExtra;
- public PluginPopupMenuItem(Context ctx, string pluginPackage, string fieldId, string displayText, int iconId, Bundle bundleExtra)
+ public PluginPopupMenuItem(EntryActivity activity, string pluginPackage, string fieldId, string popupItemId, string displayText, int iconId, Bundle bundleExtra)
{
- _ctx = ctx;
+ _activity = activity;
_pluginPackage = pluginPackage;
_fieldId = fieldId;
+ _popupItemId = popupItemId;
_displayText = displayText;
_iconId = iconId;
_bundleExtra = bundleExtra;
@@ -26,22 +31,29 @@ namespace keepass2android
public Drawable Icon
{
- get { return _ctx.PackageManager.GetResourcesForApplication(_pluginPackage).GetDrawable(_iconId); }
+ get { return _activity.PackageManager.GetResourcesForApplication(_pluginPackage).GetDrawable(_iconId); }
}
public string Text
{
get { return _displayText; }
}
+
+ public string PopupItemId
+ {
+ get { return _popupItemId; }
+ }
+
public void HandleClick()
{
Intent i = new Intent(Strings.ActionEntryActionSelected);
i.SetPackage(_pluginPackage);
i.PutExtra(Strings.ExtraActionData, _bundleExtra);
i.PutExtra(Strings.ExtraFieldId, _fieldId);
- i.PutExtra(Strings.ExtraSender, _ctx.PackageName);
- PluginHost.AddEntryToIntent(i, Entry);
+ i.PutExtra(Strings.ExtraSender, _activity.PackageName);
- _ctx.SendBroadcast(i);
+ _activity.AddEntryToIntent(i);
+
+ _activity.SendBroadcast(i);
}
}
}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/ToggleVisibilityPopupMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/ToggleVisibilityPopupMenuItem.cs
new file mode 100644
index 00000000..54ff1890
--- /dev/null
+++ b/src/PluginHostTest/EntryActivityClasses/ToggleVisibilityPopupMenuItem.cs
@@ -0,0 +1,46 @@
+using Android.Graphics.Drawables;
+using PluginHostTest;
+
+namespace keepass2android
+{
+ ///
+ /// Reperesents the popup menu item in EntryActivity to toggle visibility of all protected strings (e.g. Password)
+ ///
+ class ToggleVisibilityPopupMenuItem : IPopupMenuItem
+ {
+ private readonly EntryActivity _activity;
+
+
+ public ToggleVisibilityPopupMenuItem(EntryActivity activity)
+ {
+ _activity = activity;
+
+ }
+
+ public Drawable Icon
+ {
+ get
+ {
+ //return new TextDrawable("\uF06E", _activity);
+ return _activity.Resources.GetDrawable(Resource.Drawable.ic_action_eye_open);
+
+ }
+ }
+ public string Text
+ {
+ get
+ {
+ return _activity.Resources.GetString(
+ _activity._showPassword ?
+ Resource.String.menu_hide_password
+ : Resource.String.show_password);
+ }
+ }
+
+
+ public void HandleClick()
+ {
+ _activity.ToggleVisibility();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs b/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs
index 5d7f0c98..a90fc61b 100644
--- a/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs
+++ b/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs
@@ -3,6 +3,9 @@ using PluginHostTest;
namespace keepass2android
{
+ ///
+ /// Represents the popup menu item in EntryActivity to store the binary attachment on SD card
+ ///
internal class WriteBinaryToFilePopupItem : IPopupMenuItem
{
private readonly string _key;
diff --git a/src/PluginHostTest/PluginArrayAdapter.cs b/src/PluginHostTest/PluginArrayAdapter.cs
index 5bdda435..953fa198 100644
--- a/src/PluginHostTest/PluginArrayAdapter.cs
+++ b/src/PluginHostTest/PluginArrayAdapter.cs
@@ -10,7 +10,9 @@ using PluginHostTest;
namespace keepass2android
{
-
+ ///
+ /// Represents information about a plugin for display in the plugin list activity
+ ///
public class PluginItem
{
private readonly string _package;
diff --git a/src/PluginHostTest/PluginDatabase.cs b/src/PluginHostTest/PluginDatabase.cs
index 9a54c5fe..5b24be28 100644
--- a/src/PluginHostTest/PluginDatabase.cs
+++ b/src/PluginHostTest/PluginDatabase.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Android.Content;
+using Android.Content.PM;
using Android.Util;
using Keepass2android.Pluginsdk;
@@ -32,16 +33,15 @@ namespace keepass2android
var editor = prefs.Edit();
editor.PutString(_requesttoken, Guid.NewGuid().ToString());
editor.Commit();
-
- var hostPrefs = GetHostPrefs();
- var plugins = hostPrefs.GetStringSet(_pluginlist, new List());
- if (!plugins.Contains(packageName))
- {
- plugins.Add(packageName);
- hostPrefs.Edit().PutStringSet(_pluginlist, plugins).Commit();
- }
-
}
+ var hostPrefs = GetHostPrefs();
+ var plugins = hostPrefs.GetStringSet(_pluginlist, new List());
+ if (!plugins.Contains(packageName))
+ {
+ plugins.Add(packageName);
+ hostPrefs.Edit().PutStringSet(_pluginlist, plugins).Commit();
+ }
+
return prefs;
}
@@ -63,7 +63,20 @@ namespace keepass2android
public IEnumerable GetAllPluginPackages()
{
var hostPrefs = GetHostPrefs();
- return hostPrefs.GetStringSet(_pluginlist, new List());
+ return hostPrefs.GetStringSet(_pluginlist, new List()).Where(IsPackageInstalled);
+ }
+
+ public bool IsPackageInstalled(string targetPackage)
+ {
+ try
+ {
+ PackageInfo info = _ctx.PackageManager.GetPackageInfo(targetPackage, PackageInfoFlags.MetaData);
+ }
+ catch (PackageManager.NameNotFoundException e)
+ {
+ return false;
+ }
+ return true;
}
public bool IsEnabled(string pluginPackage)
diff --git a/src/PluginHostTest/PluginHost.cs b/src/PluginHostTest/PluginHost.cs
index b867313d..0c759e0e 100644
--- a/src/PluginHostTest/PluginHost.cs
+++ b/src/PluginHostTest/PluginHost.cs
@@ -13,6 +13,7 @@ using Android.Util;
using Android.Views;
using Android.Widget;
using KeePassLib;
+using KeePassLib.Collections;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using Keepass2android;
@@ -142,7 +143,7 @@ namespace keepass2android
return true;
}
- public static void AddEntryToIntent(Intent intent, PwEntry entry)
+ public static void AddEntryToIntent(Intent intent, PwEntryOutput entry)
{
/*//add the entry XML
not yet implemented. What to do with attachments?
@@ -151,22 +152,12 @@ namespace keepass2android
string entryData = StrUtil.Utf8.GetString(memStream.ToArray());
intent.PutExtra(Strings.ExtraEntryData, entryData);
*/
- //add the compiled string array (placeholders replaced taking into account the db context)
- Dictionary compiledFields = new Dictionary();
- foreach (var pair in entry.Strings)
- {
- String key = pair.Key;
+ //add the output string array (placeholders replaced taking into account the db context)
+ Dictionary outputFields = entry.OutputStrings.ToDictionary(pair => StrUtil.SafeXmlString(pair.Key), pair => pair.Value.ReadString());
- String value = entry.Strings.ReadSafe(key);
- value = SprEngine.Compile(value, new SprContext(entry, App.Kp2A.GetDb().KpDatabase, SprCompileFlags.All));
-
- compiledFields.Add(StrUtil.SafeXmlString(pair.Key), value);
-
- }
-
- JSONObject json = new JSONObject(compiledFields);
+ JSONObject json = new JSONObject(outputFields);
var jsonStr = json.ToString();
- intent.PutExtra(Strings.ExtraCompiledEntryData, jsonStr);
+ intent.PutExtra(Strings.ExtraEntryOutputData, jsonStr);
intent.PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString());
diff --git a/src/PluginHostTest/PluginHostTest.csproj b/src/PluginHostTest/PluginHostTest.csproj
index dca03023..5be6575b 100644
--- a/src/PluginHostTest/PluginHostTest.csproj
+++ b/src/PluginHostTest/PluginHostTest.csproj
@@ -60,11 +60,15 @@
+
+
+
+
diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/KeepassDefs.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/KeepassDefs.java
new file mode 100644
index 00000000..7dd2e706
--- /dev/null
+++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/KeepassDefs.java
@@ -0,0 +1,48 @@
+package keepass2android.pluginsdk;
+
+public class KeepassDefs {
+
+ ///
+ /// Default identifier string for the title field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ public static String TitleField = "Title";
+
+ ///
+ /// Default identifier string for the user name field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ public static String UserNameField = "UserName";
+
+ ///
+ /// Default identifier string for the password field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ public static String PasswordField = "Password";
+
+ ///
+ /// Default identifier string for the URL field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ public static String UrlField = "URL";
+
+ ///
+ /// Default identifier string for the notes field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ public static String NotesField = "Notes";
+
+
+ public static boolean IsStandardField(String strFieldName)
+ {
+ if(strFieldName == null)
+ return false;
+ if(strFieldName.equals(TitleField)) return true;
+ if(strFieldName.equals(UserNameField)) return true;
+ if(strFieldName.equals(PasswordField)) return true;
+ if(strFieldName.equals(UrlField)) return true;
+ if(strFieldName.equals(NotesField)) return true;
+
+ return false;
+ }
+}
diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessException.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessException.java
new file mode 100644
index 00000000..5f2a73ad
--- /dev/null
+++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessException.java
@@ -0,0 +1,21 @@
+package keepass2android.pluginsdk;
+
+import java.util.ArrayList;
+
+public class PluginAccessException extends Exception {
+
+ public PluginAccessException(String what)
+ {
+ super(what);
+ }
+
+ public PluginAccessException(String hostPackage, ArrayList scopes) {
+
+ }
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+}
diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginActionBroadcastReceiver.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginActionBroadcastReceiver.java
new file mode 100644
index 00000000..47dd3d6b
--- /dev/null
+++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginActionBroadcastReceiver.java
@@ -0,0 +1,224 @@
+package keepass2android.pluginsdk;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+public abstract class PluginActionBroadcastReceiver extends BroadcastReceiver {
+
+ protected abstract class PluginActionBase
+ {
+ protected Context _context;
+ protected Intent _intent;
+
+ public PluginActionBase(Context context, Intent intent)
+ {
+ _context = context;
+ _intent = intent;
+ }
+
+ public String getHostPackage() {
+ return _intent.getStringExtra(Strings.EXTRA_SENDER);
+ }
+
+ public Context getContext()
+ {
+ return _context;
+ }
+
+ protected HashMap getEntryFieldsFromIntent()
+ {
+ HashMap res = new HashMap();
+ try {
+ JSONObject json = new JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA));
+ for(Iterator iter = json.keys();iter.hasNext();) {
+ String key = iter.next();
+ String value = json.get(key).toString();
+ Log.d("KP2APluginSDK", "received " + key+"/"+value);
+ res.put(key, value);
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return res;
+ }
+ }
+
+ protected class ActionSelected extends PluginActionBase
+ {
+ public ActionSelected(Context ctx, Intent intent) {
+ super(ctx, intent);
+
+ }
+
+ /**
+ *
+ * @return the Bundle associated with the action. This bundle can be set in OpenEntry.add(Entry)FieldAction
+ */
+ public Bundle getActionData()
+ {
+ return _intent.getBundleExtra(Strings.EXTRA_ACTION_DATA);
+ }
+
+ /**
+ *
+ * @return the field id which was selected. null if an entry action (in the options menu) was selected.
+ */
+ public String getFieldId()
+ {
+ return _intent.getStringExtra(Strings.EXTRA_FIELD_ID);
+ }
+
+ /**
+ *
+ * @return true if an entry action, i.e. an option from the options menu, was selected. False if an option
+ * in a popup menu for a certain field was selected.
+ */
+ public boolean isEntryAction()
+ {
+ return getFieldId() == null;
+ }
+
+ public HashMap getEntryFields()
+ {
+ return getEntryFieldsFromIntent();
+ }
+ }
+
+ protected class CloseEntryView extends PluginActionBase
+ {
+ public CloseEntryView(Context context, Intent intent) {
+ super(context, intent);
+ }
+
+ public String getEntryId()
+ {
+ return _intent.getStringExtra(Strings.EXTRA_ENTRY_ID);
+ }
+ }
+
+ protected class OpenEntry extends PluginActionBase
+ {
+
+ public OpenEntry(Context context, Intent intent)
+ {
+ super(context, intent);
+ }
+
+ public String getEntryId()
+ {
+ return _intent.getStringExtra(Strings.EXTRA_ENTRY_ID);
+ }
+
+ public HashMap getEntryFields()
+ {
+ return getEntryFieldsFromIntent();
+ }
+
+ public void addEntryAction(String actionDisplayText, int actionIconResourceId, Bundle actionData) throws PluginAccessException
+ {
+ addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData);
+ }
+
+ public void addEntryFieldAction(String actionId, String fieldId, String actionDisplayText, int actionIconResourceId, Bundle actionData) throws PluginAccessException
+ {
+ Intent i = new Intent(Strings.ACTION_ADD_ENTRY_ACTION);
+ ArrayList scope = new ArrayList();
+ scope.add(Strings.SCOPE_CURRENT_ENTRY);
+ i.putExtra(Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(_context, getHostPackage(), scope));
+ i.setPackage(getHostPackage());
+ i.putExtra(Strings.EXTRA_SENDER, _context.getPackageName());
+ i.putExtra(Strings.EXTRA_ACTION_DATA, actionData);
+ i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText);
+ i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId);
+ i.putExtra(Strings.EXTRA_ENTRY_ID, getEntryId());
+ i.putExtra(Strings.EXTRA_FIELD_ID, fieldId);
+ i.putExtra(Strings.EXTRA_ACTION_ID, actionId);
+
+ _context.sendBroadcast(i);
+ }
+
+ public void setEntryField(String fieldId, String fieldValue, boolean isProtected) throws PluginAccessException
+ {
+ Intent i = new Intent(Strings.ACTION_SET_ENTRY_FIELD);
+ ArrayList scope = new ArrayList();
+ scope.add(Strings.SCOPE_CURRENT_ENTRY);
+ i.putExtra(Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(_context, getHostPackage(), scope));
+ i.setPackage(getHostPackage());
+ i.putExtra(Strings.EXTRA_SENDER, _context.getPackageName());
+ i.putExtra(Strings.EXTRA_FIELD_VALUE, fieldValue);
+ i.putExtra(Strings.EXTRA_ENTRY_ID, getEntryId());
+ i.putExtra(Strings.EXTRA_FIELD_ID, fieldId);
+ i.putExtra(Strings.EXTRA_FIELD_PROTECTED, isProtected);
+
+ _context.sendBroadcast(i);
+ }
+
+
+ }
+
+ //EntryOutputModified is very similar to OpenEntry because it receives the same
+ //data (+ the field id which was modified)
+ protected class EntryOutputModified extends OpenEntry
+ {
+
+ public EntryOutputModified(Context context, Intent intent)
+ {
+ super(context, intent);
+ }
+
+ public String getModifiedFieldId()
+ {
+ return _intent.getStringExtra(Strings.EXTRA_FIELD_ID);
+ }
+ }
+
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ String action = intent.getAction();
+ android.util.Log.d("KP2A.pluginsdk", "received broadcast in PluginActionBroadcastReceiver with action="+action);
+ if (action == null)
+ return;
+ if (action.equals(Strings.ACTION_OPEN_ENTRY))
+ {
+ openEntry(new OpenEntry(ctx, intent));
+ }
+ else if (action.equals(Strings.ACTION_CLOSE_ENTRY_VIEW))
+ {
+ closeEntryView(new CloseEntryView(ctx, intent));
+ }
+ else if (action.equals(Strings.ACTION_ENTRY_ACTION_SELECTED))
+ {
+ actionSelected(new ActionSelected(ctx, intent));
+ }
+ else if (action.equals(Strings.ACTION_ENTRY_OUTPUT_MODIFIED))
+ {
+ entryOutputModified(new EntryOutputModified(ctx, intent));
+ }
+ else
+ {
+ //TODO handle unexpected action
+ }
+
+
+ }
+
+ protected void closeEntryView(CloseEntryView closeEntryView) {}
+
+ protected void actionSelected(ActionSelected actionSelected) {}
+
+ protected void openEntry(OpenEntry oe) {}
+
+ protected void entryOutputModified(EntryOutputModified eom) {}
+
+}
diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java
index 5ba3de3a..9cdd2d5e 100644
--- a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java
+++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java
@@ -52,6 +52,12 @@ public class Strings {
*/
public static final String ACTION_OPEN_ENTRY= "keepass2android.ACTION_OPEN_ENTRY";
+ /**
+ * Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
+ * The Intent contains the full new entry data.
+ */
+ public static final String ACTION_ENTRY_OUTPUT_MODIFIED= "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED";
+
/**
* Action sent from KP2A to the plugin to indicate that an entry activity was closed.
*/
@@ -69,9 +75,9 @@ public class Strings {
//public static final String EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
/**
- * Json serialized list of fields, compiled using the database context (i.e. placeholders are replaced already)
+ * Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
*/
- public static final String EXTRA_COMPILED_ENTRY_DATA = "keepass2android.EXTRA_COMPILED_ENTRY_DATA";
+ public static final String EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA";
/**
* Extra key for passing the access token (both ways)
@@ -88,6 +94,12 @@ public class Strings {
public static final String EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID";
public static final String EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID";
+
+ /**
+ * Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
+ * action with same id is replaced by the new action.
+ */
+ public static final String EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID";
/** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
public static final String EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA";
@@ -110,5 +122,6 @@ public class Strings {
public static final String PREFIX_STRING = "STRING_";
public static final String PREFIX_BINARY = "BINARY_";
+
}
diff --git a/src/java/PluginQR/.classpath b/src/java/PluginQR/.classpath
new file mode 100644
index 00000000..7bc01d9a
--- /dev/null
+++ b/src/java/PluginQR/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/java/PluginQR/.project b/src/java/PluginQR/.project
new file mode 100644
index 00000000..c47434d1
--- /dev/null
+++ b/src/java/PluginQR/.project
@@ -0,0 +1,33 @@
+
+
+ PluginQR
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/src/java/PluginQR/.settings/org.eclipse.jdt.core.prefs b/src/java/PluginQR/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..b080d2dd
--- /dev/null
+++ b/src/java/PluginQR/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/src/java/PluginQR/AndroidManifest.xml b/src/java/PluginQR/AndroidManifest.xml
new file mode 100644
index 00000000..60a5d22a
--- /dev/null
+++ b/src/java/PluginQR/AndroidManifest.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/java/PluginQR/gen/keepass2android/plugin/qr/BuildConfig.java b/src/java/PluginQR/gen/keepass2android/plugin/qr/BuildConfig.java
new file mode 100644
index 00000000..71caf12e
--- /dev/null
+++ b/src/java/PluginQR/gen/keepass2android/plugin/qr/BuildConfig.java
@@ -0,0 +1,6 @@
+/** Automatically generated file. DO NOT MODIFY */
+package keepass2android.plugin.qr;
+
+public final class BuildConfig {
+ public final static boolean DEBUG = true;
+}
\ No newline at end of file
diff --git a/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java b/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java
new file mode 100644
index 00000000..94fe01ca
--- /dev/null
+++ b/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java
@@ -0,0 +1,93 @@
+/* AUTO-GENERATED FILE. DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found. It
+ * should not be modified by hand.
+ */
+
+package keepass2android.plugin.qr;
+
+public final class R {
+ public static final class attr {
+ }
+ public static final class dimen {
+ /** Default screen margins, per the Android Design guidelines.
+
+ Example customization of dimensions originally defined in res/values/dimens.xml
+ (such as screen margins) for screens with more than 820dp of available width. This
+ would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively).
+
+ */
+ public static final int activity_horizontal_margin=0x7f060000;
+ public static final int activity_vertical_margin=0x7f060001;
+ }
+ public static final class drawable {
+ public static final int ic_launcher=0x7f020000;
+ public static final int qrcode=0x7f020001;
+ }
+ public static final class id {
+ public static final int cbIncludeLabel=0x7f080003;
+ public static final int container=0x7f080000;
+ public static final int expanded_image=0x7f080005;
+ public static final int qrView=0x7f080002;
+ public static final int spinner=0x7f080001;
+ public static final int tvError=0x7f080004;
+ }
+ public static final class layout {
+ public static final int activity_qr=0x7f030000;
+ public static final int fragment_qr=0x7f030001;
+ }
+ public static final class menu {
+ public static final int qr=0x7f070000;
+ }
+ public static final class string {
+ public static final int action_settings=0x7f040002;
+ public static final int action_show_qr=0x7f040003;
+ public static final int all_fields=0x7f040005;
+ public static final int app_name=0x7f040000;
+ public static final int include_label=0x7f040004;
+ public static final int kp2aplugin_author=0x7f040008;
+ public static final int kp2aplugin_shortdesc=0x7f040007;
+ public static final int kp2aplugin_title=0x7f040006;
+ public static final int title_activity_qr=0x7f040001;
+ }
+ public static final class style {
+ /**
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+
+
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+
+
+ Base application theme for API 11+. This theme completely replaces
+ AppBaseTheme from res/values/styles.xml on API 11+ devices.
+
+ API 11 theme customizations can go here.
+
+ Base application theme for API 14+. This theme completely replaces
+ AppBaseTheme from BOTH res/values/styles.xml and
+ res/values-v11/styles.xml on API 14+ devices.
+
+ API 14 theme customizations can go here.
+
+ Base application theme, dependent on API level. This theme is replaced
+ by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+
+
+ Theme customizations available in newer API levels can go in
+ res/values-vXX/styles.xml, while customizations related to
+ backward-compatibility can go here.
+
+ */
+ public static final int AppBaseTheme=0x7f050000;
+ /** Application theme.
+ All customizations that are NOT specific to a particular API-level can go here.
+ Application theme.
+ All customizations that are NOT specific to a particular API-level can go here.
+ */
+ public static final int AppTheme=0x7f050001;
+ }
+}
diff --git a/src/java/PluginQR/gen/keepass2android/pluginsdk/R.java b/src/java/PluginQR/gen/keepass2android/pluginsdk/R.java
new file mode 100644
index 00000000..9d578f27
--- /dev/null
+++ b/src/java/PluginQR/gen/keepass2android/pluginsdk/R.java
@@ -0,0 +1,20 @@
+/* AUTO-GENERATED FILE. DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found. It
+ * should not be modified by hand.
+ */
+package keepass2android.pluginsdk;
+
+public final class R {
+ public static final class drawable {
+ public static final int ic_launcher = 0x7f020000;
+ }
+ public static final class string {
+ public static final int app_name = 0x7f040000;
+ }
+ public static final class style {
+ public static final int AppBaseTheme = 0x7f050000;
+ public static final int AppTheme = 0x7f050001;
+ }
+}
diff --git a/src/java/PluginQR/libs/core.jar b/src/java/PluginQR/libs/core.jar
new file mode 100644
index 00000000..c283af7e
Binary files /dev/null and b/src/java/PluginQR/libs/core.jar differ
diff --git a/src/java/PluginQR/proguard-project.txt b/src/java/PluginQR/proguard-project.txt
new file mode 100644
index 00000000..f2fe1559
--- /dev/null
+++ b/src/java/PluginQR/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/src/java/PluginQR/project.properties b/src/java/PluginQR/project.properties
new file mode 100644
index 00000000..b983a093
--- /dev/null
+++ b/src/java/PluginQR/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-19
+android.library.reference.1=../Keepass2AndroidPluginSDK
diff --git a/src/java/PluginQR/res/drawable-hdpi/qrcode.png b/src/java/PluginQR/res/drawable-hdpi/qrcode.png
new file mode 100644
index 00000000..86da774f
Binary files /dev/null and b/src/java/PluginQR/res/drawable-hdpi/qrcode.png differ
diff --git a/src/java/PluginQR/res/drawable-xhdpi/qrcode.png b/src/java/PluginQR/res/drawable-xhdpi/qrcode.png
new file mode 100644
index 00000000..d871b5be
Binary files /dev/null and b/src/java/PluginQR/res/drawable-xhdpi/qrcode.png differ
diff --git a/src/java/PluginQR/res/layout/activity_qr.xml b/src/java/PluginQR/res/layout/activity_qr.xml
new file mode 100644
index 00000000..24ee86c8
--- /dev/null
+++ b/src/java/PluginQR/res/layout/activity_qr.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/src/java/PluginQR/res/layout/fragment_qr.xml b/src/java/PluginQR/res/layout/fragment_qr.xml
new file mode 100644
index 00000000..4e9b5fe4
--- /dev/null
+++ b/src/java/PluginQR/res/layout/fragment_qr.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/java/PluginQR/res/menu/qr.xml b/src/java/PluginQR/res/menu/qr.xml
new file mode 100644
index 00000000..30a0acf3
--- /dev/null
+++ b/src/java/PluginQR/res/menu/qr.xml
@@ -0,0 +1,7 @@
+
diff --git a/src/java/PluginQR/res/values-w820dp/dimens.xml b/src/java/PluginQR/res/values-w820dp/dimens.xml
new file mode 100644
index 00000000..f3e70203
--- /dev/null
+++ b/src/java/PluginQR/res/values-w820dp/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+ 64dp
+
+
diff --git a/src/java/PluginQR/res/values/dimens.xml b/src/java/PluginQR/res/values/dimens.xml
new file mode 100644
index 00000000..55c1e590
--- /dev/null
+++ b/src/java/PluginQR/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+
+
+ 16dp
+ 16dp
+
+
diff --git a/src/java/PluginQR/res/values/strings.xml b/src/java/PluginQR/res/values/strings.xml
new file mode 100644
index 00000000..ec787df3
--- /dev/null
+++ b/src/java/PluginQR/res/values/strings.xml
@@ -0,0 +1,18 @@
+
+
+
+ QR Plugin for KP2A
+ QRActivity
+ Settings
+
+ Show QR Code
+ Include field label
+ All fields
+
+ QR Plugin
+ Displays password entries as QR code
+ Philipp Crocoll
+
+
+
+
diff --git a/src/java/PluginQR/res/values/styles.xml b/src/java/PluginQR/res/values/styles.xml
new file mode 100644
index 00000000..6ce89c7b
--- /dev/null
+++ b/src/java/PluginQR/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java b/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java
new file mode 100644
index 00000000..d9db29c7
--- /dev/null
+++ b/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java
@@ -0,0 +1,17 @@
+package keepass2android.plugin.qr;
+
+import java.util.ArrayList;
+
+import keepass2android.pluginsdk.PluginAccessBroadcastReceiver;
+import keepass2android.pluginsdk.Strings;
+
+public class AccessReceiver extends PluginAccessBroadcastReceiver {
+
+ @Override
+ public ArrayList getScopes() {
+ ArrayList scopes = new ArrayList();
+ scopes.add(Strings.SCOPE_CURRENT_ENTRY);
+ return scopes;
+ }
+
+}
diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/ActionReceiver.java b/src/java/PluginQR/src/keepass2android/plugin/qr/ActionReceiver.java
new file mode 100644
index 00000000..3b65fd30
--- /dev/null
+++ b/src/java/PluginQR/src/keepass2android/plugin/qr/ActionReceiver.java
@@ -0,0 +1,49 @@
+package keepass2android.plugin.qr;
+
+import org.json.JSONObject;
+
+import android.content.Intent;
+import android.widget.Toast;
+import keepass2android.pluginsdk.PluginAccessException;
+import keepass2android.pluginsdk.PluginActionBroadcastReceiver;
+import keepass2android.pluginsdk.Strings;
+
+public class ActionReceiver extends PluginActionBroadcastReceiver{
+ @Override
+ protected void openEntry(OpenEntry oe) {
+ try {
+ oe.addEntryAction(oe.getContext().getString(R.string.action_show_qr),
+ R.drawable.qrcode, null);
+
+ for (String field: oe.getEntryFields().keySet())
+ {
+ oe.addEntryFieldAction("keepass2android.plugin.qr.show", Strings.PREFIX_STRING+field, oe.getContext().getString(R.string.action_show_qr),
+ R.drawable.qrcode, null);
+ }
+ } catch (PluginAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void actionSelected(ActionSelected actionSelected) {
+ Intent i = new Intent(actionSelected.getContext(), QRActivity.class);
+ i.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, new JSONObject(actionSelected.getEntryFields()).toString());
+ i.putExtra(Strings.EXTRA_FIELD_ID, actionSelected.getFieldId());
+ i.putExtra(Strings.EXTRA_SENDER, actionSelected.getHostPackage());
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ actionSelected.getContext().startActivity(i);
+ }
+
+ @Override
+ protected void entryOutputModified(EntryOutputModified eom) {
+ try {
+ eom.addEntryFieldAction("keepass2android.plugin.qr.show", eom.getModifiedFieldId(), eom.getContext().getString(R.string.action_show_qr),
+ R.drawable.qrcode, null);
+ } catch (PluginAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+}
diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/Contents.java b/src/java/PluginQR/src/keepass2android/plugin/qr/Contents.java
new file mode 100644
index 00000000..be555e3c
--- /dev/null
+++ b/src/java/PluginQR/src/keepass2android/plugin/qr/Contents.java
@@ -0,0 +1,74 @@
+//
+// * Copyright (C) 2008 ZXing authors
+// *
+// * Licensed under the Apache License, Version 2.0 (the "License");
+// * you may not use this file except in compliance with the License.
+// * You may obtain a copy of the License at
+// *
+// * http://www.apache.org/licenses/LICENSE-2.0
+// *
+// * Unless required by applicable law or agreed to in writing, software
+// * distributed under the License is distributed on an "AS IS" BASIS,
+// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// * See the License for the specific language governing permissions and
+// * limitations under the License.
+//
+package keepass2android.plugin.qr;
+
+import android.provider.ContactsContract;
+
+public final class Contents {
+ private Contents() {
+ }
+
+ public static final class Type {
+
+ // Plain text. Use Intent.putExtra(DATA, string). This can be used for URLs too, but string
+ // must include "http://" or "https://".
+ public static final String TEXT = "TEXT_TYPE";
+
+ // An email type. Use Intent.putExtra(DATA, string) where string is the email address.
+ public static final String EMAIL = "EMAIL_TYPE";
+
+ // Use Intent.putExtra(DATA, string) where string is the phone number to call.
+ public static final String PHONE = "PHONE_TYPE";
+
+ // An SMS type. Use Intent.putExtra(DATA, string) where string is the number to SMS.
+ public static final String SMS = "SMS_TYPE";
+
+ public static final String CONTACT = "CONTACT_TYPE";
+
+ public static final String LOCATION = "LOCATION_TYPE";
+
+ private Type() {
+ }
+ }
+
+ public static final String URL_KEY = "URL_KEY";
+
+ public static final String NOTE_KEY = "NOTE_KEY";
+
+ // When using Type.CONTACT, these arrays provide the keys for adding or retrieving multiple phone numbers and addresses.
+ public static final String[] PHONE_KEYS = {
+ ContactsContract.Intents.Insert.PHONE, ContactsContract.Intents.Insert.SECONDARY_PHONE,
+ ContactsContract.Intents.Insert.TERTIARY_PHONE
+ };
+
+ public static final String[] PHONE_TYPE_KEYS = {
+ ContactsContract.Intents.Insert.PHONE_TYPE,
+ ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE,
+ ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
+ };
+
+ public static final String[] EMAIL_KEYS = {
+ ContactsContract.Intents.Insert.EMAIL, ContactsContract.Intents.Insert.SECONDARY_EMAIL,
+ ContactsContract.Intents.Insert.TERTIARY_EMAIL
+ };
+
+ public static final String[] EMAIL_TYPE_KEYS = {
+ ContactsContract.Intents.Insert.EMAIL_TYPE,
+ ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE,
+ ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE
+ };
+}
+
diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java b/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java
new file mode 100644
index 00000000..0f6484f4
--- /dev/null
+++ b/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java
@@ -0,0 +1,455 @@
+package keepass2android.plugin.qr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import keepass2android.pluginsdk.AccessManager;
+import keepass2android.pluginsdk.KeepassDefs;
+import keepass2android.pluginsdk.Strings;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.app.ActionBar;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.os.Build;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+
+public class QRActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if ((getIntent() != null) && (getIntent().getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!= null))
+ Log.d("QR", getIntent().getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA));
+
+ setContentView(R.layout.activity_qr);
+
+ if (savedInstanceState == null) {
+ getFragmentManager().beginTransaction()
+ .add(R.id.container, new PlaceholderFragment()).commit();
+ }
+
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.qr, menu);
+ return true;
+ }
+
+ /**
+ * A placeholder fragment containing a simple view.
+ */
+ public static class PlaceholderFragment extends Fragment {
+
+ // Hold a reference to the current animator,
+ // so that it can be canceled mid-way.
+ private Animator mCurrentAnimator;
+
+
+ private int mShortAnimationDuration;
+
+ Bitmap mBitmap;
+ ImageView mImageView;
+ TextView mErrorView;
+ HashMap mEntryOutput;
+ ArrayList mFieldList = new ArrayList();
+ Spinner mSpinner;
+ String mHostname;
+
+ private CheckBox mCbIncludeLabel;
+
+
+ private Resources kp2aRes;
+
+ public PlaceholderFragment() {
+ }
+
+ protected HashMap getEntryFieldsFromIntent(Intent intent)
+ {
+ HashMap res = new HashMap();
+ try {
+ JSONObject json = new JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA));
+ for(Iterator iter = json.keys();iter.hasNext();) {
+ String key = iter.next();
+ String value = json.get(key).toString();
+ res.put(key, value);
+ }
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ return res;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_qr, container,
+ false);
+
+ mSpinner = (Spinner) rootView.findViewById(R.id.spinner);
+
+ mEntryOutput = getEntryFieldsFromIntent(getActivity().getIntent());
+
+ ArrayList spinnerItems = new ArrayList();
+ spinnerItems.add(getActivity().getString(R.string.all_fields));
+ mFieldList.add(null); //all fields
+
+ try {
+ mHostname = getActivity().getIntent().getStringExtra(Strings.EXTRA_SENDER);
+ kp2aRes = getActivity().getPackageManager().getResourcesForApplication(mHostname);
+ } catch (NameNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ addIfExists(KeepassDefs.UserNameField, "entry_user_name", spinnerItems);
+ addIfExists(KeepassDefs.UrlField, "entry_url", spinnerItems);
+ addIfExists(KeepassDefs.PasswordField, "entry_password", spinnerItems);
+ addIfExists(KeepassDefs.TitleField, "entry_title", spinnerItems);
+ addIfExists(KeepassDefs.NotesField, "entry_comment", spinnerItems);
+
+ //add non-standard fields:
+ ArrayList allKeys = new ArrayList(mEntryOutput.keySet());
+ Collections.sort(allKeys);
+
+ for (String k: allKeys)
+ {
+ if (!KeepassDefs.IsStandardField(k))
+ {
+ if (!TextUtils.isEmpty(mEntryOutput.get(k)))
+ mFieldList.add(k);
+ spinnerItems.add(k);
+ }
+ }
+
+ mCbIncludeLabel = (CheckBox)rootView.findViewById(R.id.cbIncludeLabel);
+
+ boolean includeLabel = PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("includeLabels", false);
+ mCbIncludeLabel.setChecked(includeLabel);
+ mCbIncludeLabel.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+
+ updateQrCode(buildQrData(mFieldList.get( mSpinner.getSelectedItemPosition() )));
+ }
+ });
+
+ ArrayAdapter adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_spinner_item, spinnerItems);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mSpinner.setAdapter(adapter);
+
+ mImageView = ((ImageView)rootView.findViewById(R.id.qrView));
+ mErrorView = ((TextView)rootView.findViewById(R.id.tvError));
+ String fieldId = null;
+
+ if (getActivity().getIntent() != null)
+ {
+ fieldId = getActivity().getIntent().getStringExtra(Strings.EXTRA_FIELD_ID);
+ if (fieldId != null)
+ {
+ fieldId = fieldId.substring(Strings.PREFIX_STRING.length());
+ }
+ }
+ updateQrCode(buildQrData(fieldId));
+
+ mImageView.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ zoomImageFromThumb();
+ }
+ });
+
+ mSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
+
+ @Override
+ public void onItemSelected(AdapterView> arg0, View arg1,
+ int arg2, long arg3) {
+ if (arg2 != 0)
+ mCbIncludeLabel.setVisibility(View.VISIBLE);
+ else
+ mCbIncludeLabel.setVisibility(View.GONE);
+ updateQrCode(buildQrData(mFieldList.get(arg2)));
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> arg0) {
+
+ }
+ });
+
+ mSpinner.setSelection(mFieldList.indexOf(fieldId));
+
+ mShortAnimationDuration = getResources().getInteger(
+ android.R.integer.config_shortAnimTime);
+
+
+
+ return rootView;
+ }
+
+ private void addIfExists(String fieldKey, String resKey,
+ ArrayList spinnerItems) {
+ if (!TextUtils.isEmpty(mEntryOutput.get(fieldKey)))
+ {
+ mFieldList.add(fieldKey);
+ String displayString = fieldKey;
+ try
+ {
+ displayString = kp2aRes.getString(kp2aRes.getIdentifier(resKey, "string", mHostname));
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ spinnerItems.add(displayString);
+ }
+
+
+ }
+
+ private String buildQrData(String fieldId) {
+ String res = "";
+
+ if (fieldId == null)
+ {
+ res = "kp2a:\n";
+ for (String k:mFieldList)
+ {
+ if (k == null)
+ continue;
+ res += QRCodeEncoder.escapeMECARD(k)+":";
+ res += QRCodeEncoder.escapeMECARD(mEntryOutput.get(k))+";\n";
+ }
+ }
+ else
+ {
+ if ((mCbIncludeLabel.isChecked()))
+ {
+ res = fieldId+": ";
+
+ }
+ res += mEntryOutput.get(fieldId);
+ }
+
+ return res;
+ }
+
+ private void updateQrCode(String qrData) {
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager) getActivity().getSystemService(Context.WINDOW_SERVICE); // the results will be higher than using the activity context object or the getWindowManager() shortcut
+ wm.getDefaultDisplay().getMetrics(displayMetrics);
+ int screenWidth = displayMetrics.widthPixels;
+ int screenHeight = displayMetrics.heightPixels;
+
+ int qrCodeDimension = screenWidth > screenHeight ? screenHeight : screenWidth;
+ QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrData, null,
+ Contents.Type.TEXT, BarcodeFormat.QR_CODE.toString(), qrCodeDimension);
+
+
+
+ try {
+ mBitmap = qrCodeEncoder.encodeAsBitmap();
+ mImageView.setImageBitmap(mBitmap);
+ mImageView.setVisibility(View.VISIBLE);
+ mErrorView.setVisibility(View.GONE);
+ } catch (WriterException e) {
+ e.printStackTrace();
+ mErrorView.setText("Error: "+e.getMessage());
+ mErrorView.setVisibility(View.VISIBLE);
+ mImageView.setVisibility(View.GONE);
+ }
+ }
+
+ private void zoomImageFromThumb() {
+ // If there's an animation in progress, cancel it
+ // immediately and proceed with this one.
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+
+ // Load the high-resolution "zoomed-in" image.
+ final ImageView expandedImageView = (ImageView) getActivity().findViewById(
+ R.id.expanded_image);
+ expandedImageView.setImageBitmap(mBitmap);
+
+ // Calculate the starting and ending bounds for the zoomed-in image.
+ // This step involves lots of math. Yay, math.
+ final Rect startBounds = new Rect();
+ final Rect finalBounds = new Rect();
+ final Point globalOffset = new Point();
+
+ // The start bounds are the global visible rectangle of the thumbnail,
+ // and the final bounds are the global visible rectangle of the container
+ // view. Also set the container view's offset as the origin for the
+ // bounds, since that's the origin for the positioning animation
+ // properties (X, Y).
+ mImageView.getGlobalVisibleRect(startBounds);
+ getActivity().findViewById(R.id.container)
+ .getGlobalVisibleRect(finalBounds, globalOffset);
+ startBounds.offset(-globalOffset.x, -globalOffset.y);
+ finalBounds.offset(-globalOffset.x, -globalOffset.y);
+
+ // Adjust the start bounds to be the same aspect ratio as the final
+ // bounds using the "center crop" technique. This prevents undesirable
+ // stretching during the animation. Also calculate the start scaling
+ // factor (the end scaling factor is always 1.0).
+ float startScale;
+ if ((float) finalBounds.width() / finalBounds.height()
+ > (float) startBounds.width() / startBounds.height()) {
+ // Extend start bounds horizontally
+ startScale = (float) startBounds.height() / finalBounds.height();
+ float startWidth = startScale * finalBounds.width();
+ float deltaWidth = (startWidth - startBounds.width()) / 2;
+ startBounds.left -= deltaWidth;
+ startBounds.right += deltaWidth;
+ } else {
+ // Extend start bounds vertically
+ startScale = (float) startBounds.width() / finalBounds.width();
+ float startHeight = startScale * finalBounds.height();
+ float deltaHeight = (startHeight - startBounds.height()) / 2;
+ startBounds.top -= deltaHeight;
+ startBounds.bottom += deltaHeight;
+ }
+
+ // Hide the thumbnail and show the zoomed-in view. When the animation
+ // begins, it will position the zoomed-in view in the place of the
+ // thumbnail.
+ mImageView.setAlpha(0f);
+ expandedImageView.setVisibility(View.VISIBLE);
+
+ // Set the pivot point for SCALE_X and SCALE_Y transformations
+ // to the top-left corner of the zoomed-in view (the default
+ // is the center of the view).
+ expandedImageView.setPivotX(0f);
+ expandedImageView.setPivotY(0f);
+
+ // Construct and run the parallel animation of the four translation and
+ // scale properties (X, Y, SCALE_X, and SCALE_Y).
+ AnimatorSet set = new AnimatorSet();
+ set
+ .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
+ startBounds.left, finalBounds.left))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
+ startBounds.top, finalBounds.top))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
+ startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,
+ View.SCALE_Y, startScale, 1f));
+ set.setDuration(mShortAnimationDuration);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentAnimator = null;
+ }
+ });
+ set.start();
+ mCurrentAnimator = set;
+
+ // Upon clicking the zoomed-in image, it should zoom back down
+ // to the original bounds and show the thumbnail instead of
+ // the expanded image.
+ final float startScaleFinal = startScale;
+ expandedImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+
+ // Animate the four positioning/sizing properties in parallel,
+ // back to their original values.
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.Y,startBounds.top))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_X, startScaleFinal))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_Y, startScaleFinal));
+ set.setDuration(mShortAnimationDuration);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mImageView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mImageView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+ });
+ set.start();
+ mCurrentAnimator = set;
+ }
+ });
+ }
+ }
+
+}
diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/QRCodeEncoder.java b/src/java/PluginQR/src/keepass2android/plugin/qr/QRCodeEncoder.java
new file mode 100644
index 00000000..ef0f7392
--- /dev/null
+++ b/src/java/PluginQR/src/keepass2android/plugin/qr/QRCodeEncoder.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package keepass2android.plugin.qr;
+
+import android.provider.ContactsContract;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.telephony.PhoneNumberUtils;
+
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.MultiFormatWriter;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+
+public final class QRCodeEncoder {
+ private static final int WHITE = 0xFFFFFFFF;
+ private static final int BLACK = 0xFF000000;
+
+ private int dimension = Integer.MIN_VALUE;
+ private String contents = null;
+ private String displayContents = null;
+ private String title = null;
+ private BarcodeFormat format = null;
+ private boolean encoded = false;
+
+ public QRCodeEncoder(String data, Bundle bundle, String type, String format, int dimension) {
+ this.dimension = dimension;
+ encoded = encodeContents(data, bundle, type, format);
+ }
+
+ public String getContents() {
+ return contents;
+ }
+
+ public String getDisplayContents() {
+ return displayContents;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ private boolean encodeContents(String data, Bundle bundle, String type, String formatString) {
+ // Default to QR_CODE if no format given.
+ format = null;
+ if (formatString != null) {
+ try {
+ format = BarcodeFormat.valueOf(formatString);
+ } catch (IllegalArgumentException iae) {
+ // Ignore it then
+ }
+ }
+ if (format == null || format == BarcodeFormat.QR_CODE) {
+ this.format = BarcodeFormat.QR_CODE;
+ encodeQRCodeContents(data, bundle, type);
+ } else if (data != null && data.length() > 0) {
+ contents = data;
+ displayContents = data;
+ title = "Text";
+ }
+ return contents != null && contents.length() > 0;
+ }
+
+ private void encodeQRCodeContents(String data, Bundle bundle, String type) {
+ if (type.equals(Contents.Type.TEXT)) {
+ if (data != null && data.length() > 0) {
+ contents = data;
+ displayContents = data;
+ title = "Text";
+ }
+ } else if (type.equals(Contents.Type.EMAIL)) {
+ data = trim(data);
+ if (data != null) {
+ contents = "mailto:" + data;
+ displayContents = data;
+ title = "E-Mail";
+ }
+ } else if (type.equals(Contents.Type.PHONE)) {
+ data = trim(data);
+ if (data != null) {
+ contents = "tel:" + data;
+ displayContents = PhoneNumberUtils.formatNumber(data);
+ title = "Phone";
+ }
+ } else if (type.equals(Contents.Type.SMS)) {
+ data = trim(data);
+ if (data != null) {
+ contents = "sms:" + data;
+ displayContents = PhoneNumberUtils.formatNumber(data);
+ title = "SMS";
+ }
+ } else if (type.equals(Contents.Type.CONTACT)) {
+ if (bundle != null) {
+ StringBuilder newContents = new StringBuilder(100);
+ StringBuilder newDisplayContents = new StringBuilder(100);
+
+ newContents.append("MECARD:");
+
+ String name = trim(bundle.getString(ContactsContract.Intents.Insert.NAME));
+ if (name != null) {
+ newContents.append("N:").append(escapeMECARD(name)).append(';');
+ newDisplayContents.append(name);
+ }
+
+ String address = trim(bundle.getString(ContactsContract.Intents.Insert.POSTAL));
+ if (address != null) {
+ newContents.append("ADR:").append(escapeMECARD(address)).append(';');
+ newDisplayContents.append('\n').append(address);
+ }
+
+ Collection uniquePhones = new HashSet(Contents.PHONE_KEYS.length);
+ for (int x = 0; x < Contents.PHONE_KEYS.length; x++) {
+ String phone = trim(bundle.getString(Contents.PHONE_KEYS[x]));
+ if (phone != null) {
+ uniquePhones.add(phone);
+ }
+ }
+ for (String phone : uniquePhones) {
+ newContents.append("TEL:").append(escapeMECARD(phone)).append(';');
+ newDisplayContents.append('\n').append(PhoneNumberUtils.formatNumber(phone));
+ }
+
+ Collection uniqueEmails = new HashSet(Contents.EMAIL_KEYS.length);
+ for (int x = 0; x < Contents.EMAIL_KEYS.length; x++) {
+ String email = trim(bundle.getString(Contents.EMAIL_KEYS[x]));
+ if (email != null) {
+ uniqueEmails.add(email);
+ }
+ }
+ for (String email : uniqueEmails) {
+ newContents.append("EMAIL:").append(escapeMECARD(email)).append(';');
+ newDisplayContents.append('\n').append(email);
+ }
+
+ String url = trim(bundle.getString(Contents.URL_KEY));
+ if (url != null) {
+ // escapeMECARD(url) -> wrong escape e.g. http\://zxing.google.com
+ newContents.append("URL:").append(url).append(';');
+ newDisplayContents.append('\n').append(url);
+ }
+
+ String note = trim(bundle.getString(Contents.NOTE_KEY));
+ if (note != null) {
+ newContents.append("NOTE:").append(escapeMECARD(note)).append(';');
+ newDisplayContents.append('\n').append(note);
+ }
+
+ // Make sure we've encoded at least one field.
+ if (newDisplayContents.length() > 0) {
+ newContents.append(';');
+ contents = newContents.toString();
+ displayContents = newDisplayContents.toString();
+ title = "Contact";
+ } else {
+ contents = null;
+ displayContents = null;
+ }
+
+ }
+ } else if (type.equals(Contents.Type.LOCATION)) {
+ if (bundle != null) {
+ // These must use Bundle.getFloat(), not getDouble(), it's part of the API.
+ float latitude = bundle.getFloat("LAT", Float.MAX_VALUE);
+ float longitude = bundle.getFloat("LONG", Float.MAX_VALUE);
+ if (latitude != Float.MAX_VALUE && longitude != Float.MAX_VALUE) {
+ contents = "geo:" + latitude + ',' + longitude;
+ displayContents = latitude + "," + longitude;
+ title = "Location";
+ }
+ }
+ }
+ }
+
+ public Bitmap encodeAsBitmap() throws WriterException {
+ if (!encoded) return null;
+
+ Map hints = null;
+ String encoding = guessAppropriateEncoding(contents);
+ hints = new EnumMap(EncodeHintType.class);
+ if (encoding != null) {
+
+ hints.put(EncodeHintType.CHARACTER_SET, encoding);
+ }
+ hints.put(EncodeHintType.MARGIN, 2); /* default = 4 */
+
+
+ MultiFormatWriter writer = new MultiFormatWriter();
+ BitMatrix result = writer.encode(contents, format, dimension, dimension, hints);
+ int width = result.getWidth();
+ int height = result.getHeight();
+ int[] pixels = new int[width * height];
+ // All are 0, or black, by default
+ for (int y = 0; y < height; y++) {
+ int offset = y * width;
+ for (int x = 0; x < width; x++) {
+ pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
+ }
+ }
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
+ return bitmap;
+ }
+
+ private static String guessAppropriateEncoding(CharSequence contents) {
+ // Very crude at the moment
+ for (int i = 0; i < contents.length(); i++) {
+ if (contents.charAt(i) > 0xFF) { return "UTF-8"; }
+ }
+ return null;
+ }
+
+ private static String trim(String s) {
+ if (s == null) { return null; }
+ String result = s.trim();
+ return result.length() == 0 ? null : result;
+ }
+
+ public static String escapeMECARD(String input) {
+ if (input == null || (input.indexOf(':') < 0 && input.indexOf(';') < 0)) { return input; }
+ int length = input.length();
+ StringBuilder result = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ char c = input.charAt(i);
+ if (c == ':' || c == ';') {
+ result.append('\\');
+ }
+ result.append(c);
+ }
+ return result.toString();
+ }
+}
+