mirror of
https://github.com/moparisthebest/keepass2android
synced 2024-12-23 07:28:48 -05:00
Plugins:
* EntryOutput is passed to CopyToClipboardService * Modifications of EntryOutput are passed to plugins to enable actions on added fields * PluginDatabase checks if Plugin is still installed and always updates the list of plugins (had an issue where a plugin had a request token but was not in pluginList) * first version of QR plugin implemented
This commit is contained in:
parent
07038d7549
commit
53dd47044b
@ -1,17 +1,74 @@
|
||||
using System;
|
||||
using KeePassLib;
|
||||
using KeePassLib.Collections;
|
||||
using KeePassLib.Keys;
|
||||
using KeePassLib.Security;
|
||||
using KeePassLib.Serialization;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the strings which are output from a PwEntry.
|
||||
/// </summary>
|
||||
/// 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();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the PwEntryOutput by replacing the placeholders
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ID of the entry
|
||||
/// </summary>
|
||||
public PwUuid Uuid
|
||||
{
|
||||
get { return Entry.Uuid; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The output strings for the represented entry
|
||||
/// </summary>
|
||||
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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to copy a string to clipboard
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
34
src/PluginHostTest/EntryActivityClasses/GotoUrlMenuItem.cs
Normal file
34
src/PluginHostTest/EntryActivityClasses/GotoUrlMenuItem.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to go to the URL in the field
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using Android.Content;
|
||||
using Android.Graphics.Drawables;
|
||||
using KeePassLib;
|
||||
using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for popup menu items in EntryActivity
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@ using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the popup menu item in EntryActivity to open the associated attachment
|
||||
/// </summary>
|
||||
internal class OpenBinaryPopupItem : IPopupMenuItem
|
||||
{
|
||||
private readonly string _key;
|
||||
|
@ -5,20 +5,25 @@ using Keepass2android.Pluginsdk;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a popup menu item in EntryActivity which was added by a plugin. The click will therefore broadcast to the plugin.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using Android.Graphics.Drawables;
|
||||
using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Reperesents the popup menu item in EntryActivity to toggle visibility of all protected strings (e.g. Password)
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@ using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the popup menu item in EntryActivity to store the binary attachment on SD card
|
||||
/// </summary>
|
||||
internal class WriteBinaryToFilePopupItem : IPopupMenuItem
|
||||
{
|
||||
private readonly string _key;
|
||||
|
@ -10,7 +10,9 @@ using PluginHostTest;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a plugin for display in the plugin list activity
|
||||
/// </summary>
|
||||
public class PluginItem
|
||||
{
|
||||
private readonly string _package;
|
||||
|
@ -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<string>());
|
||||
if (!plugins.Contains(packageName))
|
||||
{
|
||||
plugins.Add(packageName);
|
||||
hostPrefs.Edit().PutStringSet(_pluginlist, plugins).Commit();
|
||||
}
|
||||
|
||||
}
|
||||
var hostPrefs = GetHostPrefs();
|
||||
var plugins = hostPrefs.GetStringSet(_pluginlist, new List<string>());
|
||||
if (!plugins.Contains(packageName))
|
||||
{
|
||||
plugins.Add(packageName);
|
||||
hostPrefs.Edit().PutStringSet(_pluginlist, plugins).Commit();
|
||||
}
|
||||
|
||||
return prefs;
|
||||
}
|
||||
|
||||
@ -63,7 +63,20 @@ namespace keepass2android
|
||||
public IEnumerable<String> GetAllPluginPackages()
|
||||
{
|
||||
var hostPrefs = GetHostPrefs();
|
||||
return hostPrefs.GetStringSet(_pluginlist, new List<string>());
|
||||
return hostPrefs.GetStringSet(_pluginlist, new List<string>()).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)
|
||||
|
@ -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<string, string> compiledFields = new Dictionary<string, string>();
|
||||
foreach (var pair in entry.Strings)
|
||||
{
|
||||
String key = pair.Key;
|
||||
//add the output string array (placeholders replaced taking into account the db context)
|
||||
Dictionary<string, string> 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());
|
||||
|
||||
|
@ -60,11 +60,15 @@
|
||||
<Compile Include="ClickView.cs" />
|
||||
<Compile Include="CopyToClipboardService.cs" />
|
||||
<Compile Include="EntryActivity.cs" />
|
||||
<Compile Include="EntryActivityClasses\CopyToClipboardPopupMenuIcon.cs" />
|
||||
<Compile Include="EntryActivityClasses\GotoUrlMenuItem.cs" />
|
||||
<Compile Include="EntryActivityClasses\ToggleVisibilityPopupMenuItem.cs" />
|
||||
<Compile Include="EntryContentsView.cs" />
|
||||
<Compile Include="EntrySection.cs" />
|
||||
<Compile Include="EntryActivityClasses\ExtraStringView.cs" />
|
||||
<Compile Include="EntryActivityClasses\IPopupMenuItem.cs" />
|
||||
<Compile Include="EntryActivityClasses\IStringView.cs" />
|
||||
<Compile Include="Intents.cs" />
|
||||
<Compile Include="Kp2aShortHelpView.cs" />
|
||||
<Compile Include="EntryActivityClasses\OpenBinaryPopupItem.cs" />
|
||||
<Compile Include="PluginDatabase.cs" />
|
||||
|
@ -0,0 +1,48 @@
|
||||
package keepass2android.pluginsdk;
|
||||
|
||||
public class KeepassDefs {
|
||||
|
||||
/// <summary>
|
||||
/// Default identifier string for the title field. Should not contain
|
||||
/// spaces, tabs or other whitespace.
|
||||
/// </summary>
|
||||
public static String TitleField = "Title";
|
||||
|
||||
/// <summary>
|
||||
/// Default identifier string for the user name field. Should not contain
|
||||
/// spaces, tabs or other whitespace.
|
||||
/// </summary>
|
||||
public static String UserNameField = "UserName";
|
||||
|
||||
/// <summary>
|
||||
/// Default identifier string for the password field. Should not contain
|
||||
/// spaces, tabs or other whitespace.
|
||||
/// </summary>
|
||||
public static String PasswordField = "Password";
|
||||
|
||||
/// <summary>
|
||||
/// Default identifier string for the URL field. Should not contain
|
||||
/// spaces, tabs or other whitespace.
|
||||
/// </summary>
|
||||
public static String UrlField = "URL";
|
||||
|
||||
/// <summary>
|
||||
/// Default identifier string for the notes field. Should not contain
|
||||
/// spaces, tabs or other whitespace.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<String> scopes) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
}
|
@ -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<String, String> getEntryFieldsFromIntent()
|
||||
{
|
||||
HashMap<String, String> res = new HashMap<String, String>();
|
||||
try {
|
||||
JSONObject json = new JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA));
|
||||
for(Iterator<String> 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<String, String> 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<String, String> 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<String> scope = new ArrayList<String>();
|
||||
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<String> scope = new ArrayList<String>();
|
||||
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) {}
|
||||
|
||||
}
|
@ -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_";
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
9
src/java/PluginQR/.classpath
Normal file
9
src/java/PluginQR/.classpath
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
33
src/java/PluginQR/.project
Normal file
33
src/java/PluginQR/.project
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>PluginQR</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
4
src/java/PluginQR/.settings/org.eclipse.jdt.core.prefs
Normal file
4
src/java/PluginQR/.settings/org.eclipse.jdt.core.prefs
Normal file
@ -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
|
46
src/java/PluginQR/AndroidManifest.xml
Normal file
46
src/java/PluginQR/AndroidManifest.xml
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="keepass2android.plugin.qr"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="14"
|
||||
android:targetSdkVersion="19" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/qrcode"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
<activity
|
||||
android:name="keepass2android.plugin.qr.QRActivity"
|
||||
android:label="@string/title_activity_qr" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
<receiver android:name="AccessReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
||||
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
||||
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
||||
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
<receiver android:name="ActionReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_OPEN_ENTRY" />
|
||||
<action android:name="keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED" />
|
||||
<action android:name="keepass2android.ACTION_ENTRY_ACTION_SELECTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,6 @@
|
||||
/** Automatically generated file. DO NOT MODIFY */
|
||||
package keepass2android.plugin.qr;
|
||||
|
||||
public final class BuildConfig {
|
||||
public final static boolean DEBUG = true;
|
||||
}
|
93
src/java/PluginQR/gen/keepass2android/plugin/qr/R.java
Normal file
93
src/java/PluginQR/gen/keepass2android/plugin/qr/R.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
20
src/java/PluginQR/gen/keepass2android/pluginsdk/R.java
Normal file
20
src/java/PluginQR/gen/keepass2android/pluginsdk/R.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
BIN
src/java/PluginQR/libs/core.jar
Normal file
BIN
src/java/PluginQR/libs/core.jar
Normal file
Binary file not shown.
20
src/java/PluginQR/proguard-project.txt
Normal file
20
src/java/PluginQR/proguard-project.txt
Normal file
@ -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 *;
|
||||
#}
|
15
src/java/PluginQR/project.properties
Normal file
15
src/java/PluginQR/project.properties
Normal file
@ -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
|
BIN
src/java/PluginQR/res/drawable-hdpi/qrcode.png
Normal file
BIN
src/java/PluginQR/res/drawable-hdpi/qrcode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
src/java/PluginQR/res/drawable-xhdpi/qrcode.png
Normal file
BIN
src/java/PluginQR/res/drawable-xhdpi/qrcode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
8
src/java/PluginQR/res/layout/activity_qr.xml
Normal file
8
src/java/PluginQR/res/layout/activity_qr.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="keepass2android.plugin.qr.QRActivity"
|
||||
tools:ignore="MergeRootFrame" />
|
||||
|
56
src/java/PluginQR/res/layout/fragment_qr.xml
Normal file
56
src/java/PluginQR/res/layout/fragment_qr.xml
Normal file
@ -0,0 +1,56 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="keepass2android.plugin.qr.QRActivity$PlaceholderFragment" >
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true" />
|
||||
|
||||
<ImageView
|
||||
android:layout_below="@+id/spinner"
|
||||
android:id="@+id/qrView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:src="@drawable/qrcode"
|
||||
/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbIncludeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/qrView"
|
||||
android:text="@string/include_label" />
|
||||
<TextView android:id="@+id/tvError"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/cbIncludeLabel"
|
||||
/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
<ImageView
|
||||
android:id="@+id/expanded_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="invisible"/>
|
||||
|
||||
</FrameLayout>
|
7
src/java/PluginQR/res/menu/qr.xml
Normal file
7
src/java/PluginQR/res/menu/qr.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="keepass2android.plugin.qr.QRActivity" >
|
||||
|
||||
|
||||
|
||||
</menu>
|
10
src/java/PluginQR/res/values-w820dp/dimens.xml
Normal file
10
src/java/PluginQR/res/values-w820dp/dimens.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
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).
|
||||
-->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
|
||||
</resources>
|
7
src/java/PluginQR/res/values/dimens.xml
Normal file
7
src/java/PluginQR/res/values/dimens.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
</resources>
|
18
src/java/PluginQR/res/values/strings.xml
Normal file
18
src/java/PluginQR/res/values/strings.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">QR Plugin for KP2A</string>
|
||||
<string name="title_activity_qr">QRActivity</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
|
||||
<string name="action_show_qr">Show QR Code</string>
|
||||
<string name="include_label">Include field label</string>
|
||||
<string name="all_fields">All fields</string>
|
||||
|
||||
<string name="kp2aplugin_title">QR Plugin</string>
|
||||
<string name="kp2aplugin_shortdesc">Displays password entries as QR code</string>
|
||||
<string name="kp2aplugin_author">Philipp Crocoll</string>
|
||||
|
||||
|
||||
|
||||
</resources>
|
20
src/java/PluginQR/res/values/styles.xml
Normal file
20
src/java/PluginQR/res/values/styles.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<resources>
|
||||
|
||||
<!--
|
||||
Base application theme, dependent on API level. This theme is replaced
|
||||
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
|
||||
-->
|
||||
<style name="AppBaseTheme" parent="android:Theme.Light">
|
||||
<!--
|
||||
Theme customizations available in newer API levels can go in
|
||||
res/values-vXX/styles.xml, while customizations related to
|
||||
backward-compatibility can go here.
|
||||
-->
|
||||
</style>
|
||||
|
||||
<!-- Application theme. -->
|
||||
<style name="AppTheme" parent="AppBaseTheme">
|
||||
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
@ -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<String> getScopes() {
|
||||
ArrayList<String> scopes = new ArrayList<String>();
|
||||
scopes.add(Strings.SCOPE_CURRENT_ENTRY);
|
||||
return scopes;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
||||
|
455
src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java
Normal file
455
src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java
Normal file
@ -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<String, String> mEntryOutput;
|
||||
ArrayList<String> mFieldList = new ArrayList<String>();
|
||||
Spinner mSpinner;
|
||||
String mHostname;
|
||||
|
||||
private CheckBox mCbIncludeLabel;
|
||||
|
||||
|
||||
private Resources kp2aRes;
|
||||
|
||||
public PlaceholderFragment() {
|
||||
}
|
||||
|
||||
protected HashMap<String, String> getEntryFieldsFromIntent(Intent intent)
|
||||
{
|
||||
HashMap<String, String> res = new HashMap<String, String>();
|
||||
try {
|
||||
JSONObject json = new JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA));
|
||||
for(Iterator<String> 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<String> spinnerItems = new ArrayList<String>();
|
||||
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<String> allKeys = new ArrayList<String>(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<String> adapter = new ArrayAdapter<String>(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<String> 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<String> uniquePhones = new HashSet<String>(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<String> uniqueEmails = new HashSet<String>(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<EncodeHintType, Object> hints = null;
|
||||
String encoding = guessAppropriateEncoding(contents);
|
||||
hints = new EnumMap<EncodeHintType, Object>(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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user