* 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:
Philipp Crocoll 2014-05-07 05:58:20 +02:00
parent 07038d7549
commit 53dd47044b
42 changed files with 1853 additions and 137 deletions

View File

@ -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();

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View 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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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)

View File

@ -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());

View File

@ -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" />

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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) {}
}

View File

@ -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_";
}

View 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>

View 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>

View 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

View 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>

View File

@ -0,0 +1,6 @@
/** Automatically generated file. DO NOT MODIFY */
package keepass2android.plugin.qr;
public final class BuildConfig {
public final static boolean DEBUG = true;
}

View 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;
}
}

View 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;
}
}

Binary file not shown.

View 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 *;
#}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View 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" />

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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
};
}

View 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;
}
});
}
}
}

View File

@ -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();
}
}