diff --git a/src/KeePass.sln b/src/KeePass.sln index 18929f4a..b3d93c9b 100644 --- a/src/KeePass.sln +++ b/src/KeePass.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KP2AKdbLibraryBinding", "KP EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArtTestApp", "ArtTestApp\ArtTestApp.csproj", "{1FF6C335-A627-43C9-AAA7-CBAC2E74CD18}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginHostTest", "PluginHostTest\PluginHostTest.csproj", "{C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginSdkBinding", "PluginSdkBinding\PluginSdkBinding.csproj", "{3DA3911E-36DE-465E-8F15-F1991B6437E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -284,6 +288,48 @@ Global {1FF6C335-A627-43C9-AAA7-CBAC2E74CD18}.ReleaseNoNet|Mixed Platforms.Deploy.0 = Release|Any CPU {1FF6C335-A627-43C9-AAA7-CBAC2E74CD18}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU {1FF6C335-A627-43C9-AAA7-CBAC2E74CD18}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Mixed Platforms.Deploy.0 = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|Win32.ActiveCfg = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Any CPU.Build.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Any CPU.Deploy.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Mixed Platforms.Deploy.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|Win32.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.Release|x64.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Any CPU.Deploy.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Mixed Platforms.Deploy.0 = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {C9F4AE81-0996-4E17-B3F2-D0F652F6AC50}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|Win32.ActiveCfg = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|Any CPU.Build.0 = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|Win32.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.Release|x64.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {3DA3911E-36DE-465E-8F15-F1991B6437E5}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/PluginHostTest/App.cs b/src/PluginHostTest/App.cs new file mode 100644 index 00000000..614158a7 --- /dev/null +++ b/src/PluginHostTest/App.cs @@ -0,0 +1,37 @@ +using KeePassLib; +using KeePassLib.Keys; +using KeePassLib.Serialization; + +namespace keepass2android +{ + public class App + { + public class Kp2A + { + private static Db _mDb; + + public class Db + { + public void SetEntry(PwEntry e) + { + KpDatabase = new PwDatabase(); + KpDatabase.New(new IOConnectionInfo(), new CompositeKey()); + + KpDatabase.RootGroup.AddEntry(e, true); + } + + public PwDatabase KpDatabase + { + get; set; + } + } + + public static Db GetDb() + { + if (_mDb == null) + _mDb = new Db(); + return _mDb; + } + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/CopyToClipboardService.cs b/src/PluginHostTest/CopyToClipboardService.cs new file mode 100644 index 00000000..de30b26e --- /dev/null +++ b/src/PluginHostTest/CopyToClipboardService.cs @@ -0,0 +1,14 @@ +using Android.Content; +using Android.Widget; +using KeePassLib.Security; + +namespace keepass2android +{ + internal class CopyToClipboardService + { + public static void CopyValueToClipboardWithTimeout(Context ctx, string text) + { + Toast.MakeText(ctx, text, ToastLength.Short).Show(); + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivity.cs b/src/PluginHostTest/EntryActivity.cs index 0eb25ddb..5eff70f3 100644 --- a/src/PluginHostTest/EntryActivity.cs +++ b/src/PluginHostTest/EntryActivity.cs @@ -19,10 +19,12 @@ using System; using System.Collections.Generic; using System.Text; using System.Linq; - +using System.Threading; using Android.App; using Android.Content; using Android.OS; +using Android.Runtime; +using Android.Text; using Android.Views; using Android.Widget; using Android.Preferences; @@ -32,79 +34,412 @@ using Android.Content.PM; using Android.Webkit; using Android.Graphics; using Java.IO; +using KeePassLib; +using KeePassLib.Security; +using Keepass2android.Pluginsdk; using PluginHostTest; +using Uri = Android.Net.Uri; namespace keepass2android { - [Activity (Label = "@string/app_name", ConfigurationChanges=ConfigChanges.Orientation|ConfigChanges.KeyboardHidden, Theme="@style/NoTitleBar")] - public class EntryActivity : Activity { + + [Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, + Theme = "@style/NoTitleBar")] + public class EntryActivity : Activity + { public const String KeyEntry = "entry"; public const String KeyRefreshPos = "refresh_pos"; public const String KeyCloseAfterCreate = "close_after_create"; + protected PwEntry Entry = new PwEntry(true, true); + private static Typeface _passwordFont; - private bool _showPassword; + internal bool _showPassword; private int _pos; private List _protectedTextViews; + private readonly Dictionary> _popupMenuItems = + new Dictionary>(); - protected void SetEntryView() { + private readonly Dictionary _stringViews = new Dictionary(); + private readonly List _pendingMenuOptions = new List(); + private IMenu _menu; + + protected void SetEntryView() + { SetContentView(Resource.Layout.entry_view); } - - protected void SetupEditButtons() { - View edit = FindViewById(Resource.Id.entry_edit); + + protected void SetupEditButtons() + { + View edit = FindViewById(Resource.Id.entry_edit); if (true) { edit.Visibility = ViewStates.Visible; edit.Click += (sender, e) => - { - - }; + { + + }; } else { edit.Visibility = ViewStates.Gone; } - + } - + + private class PluginActionReceiver : BroadcastReceiver + { + private readonly EntryActivity _activity; + + public PluginActionReceiver(EntryActivity activity) + { + _activity = activity; + } + + public override void OnReceive(Context context, Intent intent) + { + var pluginPackage = intent.GetStringExtra(Strings.ExtraSender); + if (new PluginDatabase(context).IsValidAccessToken(pluginPackage, + intent.GetStringExtra(Strings.ExtraAccessToken), + Strings.ScopeCurrentEntry)) + { + if (intent.GetStringExtra(Strings.ExtraEntryId) != _activity.Entry.Uuid.ToHexString()) + { + Kp2aLog.Log("received action for wrong entry " + intent.GetStringExtra(Strings.ExtraEntryId)); + return; + } + _activity.AddPluginAction(pluginPackage, + intent.GetStringExtra(Strings.ExtraFieldId), + intent.GetStringExtra(Strings.ExtraActionDisplayText), + intent.GetIntExtra(Strings.ExtraActionIconResId, -1), + intent.GetBundleExtra(Strings.ExtraActionData)); + } + else + { + Kp2aLog.Log("received invalid request. Plugin not authorized."); + } + } + } + + private class PluginFieldReceiver : BroadcastReceiver + { + private readonly EntryActivity _activity; + + public PluginFieldReceiver(EntryActivity activity) + { + _activity = activity; + } + + public override void OnReceive(Context context, Intent intent) + { + if (intent.GetStringExtra(Strings.ExtraEntryId) != _activity.Entry.Uuid.ToHexString()) + { + Kp2aLog.Log("received field for wrong entry " + intent.GetStringExtra(Strings.ExtraEntryId)); + return; + } + if (!new PluginDatabase(context).IsValidAccessToken(intent.GetStringExtra(Strings.ExtraSender), + intent.GetStringExtra(Strings.ExtraAccessToken), + Strings.ScopeCurrentEntry)) + { + Kp2aLog.Log("received field with invalid access token from " + intent.GetStringExtra(Strings.ExtraSender)); + return; + } + string key = intent.GetStringExtra(Strings.ExtraFieldId); + string value = intent.GetStringExtra(Strings.ExtraFieldValue); + bool isProtected = intent.GetBooleanExtra(Strings.ExtraFieldProtected, false); + _activity.SetPluginField(key, value, isProtected); + } + } + + private void SetPluginField(string key, string value, bool isProtected) + { + IStringView existingField; + if (_stringViews.TryGetValue(key, out existingField)) + { + existingField.Text = value; + } + else + { + ViewGroup extraGroup = (ViewGroup) FindViewById(Resource.Id.extra_strings); + var view = CreateExtraSection(key, value, isProtected); + extraGroup.AddView(view.View); + } + + } + + private void AddPluginAction(string pluginPackage, string fieldId, string displayText, int iconId, Bundle bundleExtra) + { + if (fieldId != null) + { + _popupMenuItems[fieldId].Add(new PluginPopupMenuItem(this, pluginPackage, fieldId, displayText, iconId, bundleExtra)); + } + else + { + //we need to add an option to the menu. + //As it is not sure that OnCreateOptionsMenu was called yet, we cannot access _menu without a check: + + Intent i = new Intent(Strings.ActionEntryActionSelected); + i.SetPackage(pluginPackage); + i.PutExtra(Strings.ExtraActionData, bundleExtra); + i.PutExtra(Strings.ExtraSender, PackageName); + + var menuOption = new PluginMenuOption() + { + DisplayText = displayText, + Icon = PackageManager.GetResourcesForApplication(pluginPackage).GetDrawable(iconId), + Intent = i + }; + + if (_menu != null) + { + AddMenuOption(menuOption); + } + else + { + lock (_pendingMenuOptions) + { + _pendingMenuOptions.Add(menuOption); + } + + } + + + } + } + + private void AddMenuOption(PluginMenuOption menuOption) + { + var menuItem = _menu.Add(menuOption.DisplayText); + menuItem.SetIcon(menuOption.Icon); + menuItem.SetIntent(menuOption.Intent); + } + + public override bool OnCreateOptionsMenu(IMenu menu) + { + _menu = menu; + base.OnCreateOptionsMenu(menu); + + MenuInflater inflater = MenuInflater; + inflater.Inflate(Resource.Menu.entry, menu); + + lock (_pendingMenuOptions) + { + foreach (var option in _pendingMenuOptions) + AddMenuOption(option); + _pendingMenuOptions.Clear(); + } + + + UpdateTogglePasswordMenu(); + + IMenuItem gotoUrl = menu.FindItem(Resource.Id.menu_goto_url); + //Disabled IMenuItem copyUser = menu.FindItem(Resource.Id.menu_copy_user); + //Disabled IMenuItem copyPass = menu.FindItem(Resource.Id.menu_copy_pass); + + // In API >= 11 onCreateOptionsMenu may be called before onCreate completes + // so _entry may not be set + if (Entry == null) + { + gotoUrl.SetVisible(false); + //Disabled copyUser.SetVisible(false); + //Disabled copyPass.SetVisible(false); + } + else + { + String url = Entry.Strings.ReadSafe(PwDefs.UrlField); + if (String.IsNullOrEmpty(url)) + { + // disable button if url is not available + gotoUrl.SetVisible(false); + } + if (String.IsNullOrEmpty(Entry.Strings.ReadSafe(PwDefs.UserNameField))) + { + // disable button if username is not available + //Disabled copyUser.SetVisible(false); + } + if (String.IsNullOrEmpty(Entry.Strings.ReadSafe(PwDefs.PasswordField))) + { + // disable button if password is not available + //Disabled copyPass.SetVisible(false); + } + } + return true; + } + + public override bool OnOptionsItemSelected(IMenuItem item) + { + //check if this is a plugin action + if ((item.Intent != null) && (item.Intent.Action == Strings.ActionEntryActionSelected)) + { + //yes. let the plugin handle the click: + SendBroadcast(item.Intent); + return true; + } + + switch (item.ItemId) + { + case Resource.Id.menu_donate: + try + { + // Util.GotoDonateUrl(this); + } + catch (ActivityNotFoundException) + { + Toast.MakeText(this, Resource.String.error_failed_to_launch_link, ToastLength.Long).Show(); + return false; + } + + return true; + case Resource.Id.menu_toggle_pass: + if (_showPassword) + { + item.SetTitle(Resource.String.show_password); + _showPassword = false; + } + else + { + item.SetTitle(Resource.String.menu_hide_password); + _showPassword = true; + } + SetPasswordStyle(); + + return true; + + case Resource.Id.menu_goto_url: + string url = _stringViews[PwDefs.UrlField].Text; + if (url == null) return false; + + // Default http:// if no protocol specified + if (!url.Contains("://")) + { + url = "http://" + url; + } + + try + { + + } + catch (ActivityNotFoundException) + { + Toast.MakeText(this, Resource.String.no_url_handler, ToastLength.Long).Show(); + } + return true; + /* TODO: required? + case Resource.Id.menu_copy_user: + timeoutCopyToClipboard(_entry.Strings.ReadSafe (PwDefs.UserNameField)); + return true; + + case Resource.Id.menu_copy_pass: + timeoutCopyToClipboard(_entry.Strings.ReadSafe (PwDefs.UserNameField)); + return true; + */ + case Resource.Id.menu_rate: + try + { + } + catch (ActivityNotFoundException) + { + Toast.MakeText(this, Resource.String.no_url_handler, ToastLength.Long).Show(); + } + return true; + case Resource.Id.menu_suggest_improvements: + try + { + } + catch (ActivityNotFoundException) + { + Toast.MakeText(this, Resource.String.no_url_handler, ToastLength.Long).Show(); + } + return true; + case Resource.Id.menu_lock: + return true; + case Resource.Id.menu_translate: + try + { + + } + catch (ActivityNotFoundException) + { + Toast.MakeText(this, Resource.String.no_url_handler, ToastLength.Long).Show(); + } + return true; + case Android.Resource.Id.Home: + //Currently the action bar only displays the home button when we come from a previous activity. + //So we can simply Finish. See this page for information on how to do this in more general (future?) cases: + //http://developer.android.com/training/implementing-navigation/ancestral.html + Finish(); + + return true; + } + + + return base.OnOptionsItemSelected(item); + } + + protected override void OnCreate(Bundle savedInstanceState) { - + ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this); long usageCount = prefs.GetLong(GetString(Resource.String.UsageCount_key), 0); ISharedPreferencesEditor edit = prefs.Edit(); - edit.PutLong(GetString(Resource.String.UsageCount_key), usageCount+1); + edit.PutLong(GetString(Resource.String.UsageCount_key), usageCount + 1); edit.Commit(); - _showPassword = ! prefs.GetBoolean(GetString(Resource.String.maskpass_key), Resources.GetBoolean(Resource.Boolean.maskpass_default)); - + _showPassword = + !prefs.GetBoolean(GetString(Resource.String.maskpass_key), Resources.GetBoolean(Resource.Boolean.maskpass_default)); + + Entry.Strings.Set(PwDefs.UserNameField, new ProtectedString(false, "philipp ")); + Entry.Strings.Set(PwDefs.PasswordField, new ProtectedString(true, "password value")); + Entry.Strings.Set(PwDefs.UrlField, new ProtectedString(false, "https://www.google.com")); + Entry.Strings.Set("field header", new ProtectedString(true, "protected field value")); + Entry.Strings.Set("public field header", new ProtectedString(false, "public field value")); + base.OnCreate(savedInstanceState); SetEntryView(); - FillData(false); - + FillData(); + SetupEditButtons(); - + RegisterReceiver(new PluginActionReceiver(this), new IntentFilter(Strings.ActionAddEntryAction)); + RegisterReceiver(new PluginFieldReceiver(this), new IntentFilter(Strings.ActionSetEntryField)); + + new Thread(NotifyPluginsOnOpen).Start(); + } + + private void NotifyPluginsOnOpen() + { + App.Kp2A.GetDb().SetEntry(Entry); + + Intent i = new Intent(Strings.ActionOpenEntry); + i.PutExtra(Strings.ExtraSender, PackageName); + PluginHost.AddEntryToIntent(i, Entry); + + foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry)) + { + i.SetPackage(plugin); + SendBroadcast(i); + } } public void CompleteOnCreate() { - + } - private String getDateTime(DateTime dt) { - return dt.ToString ("g", CultureInfo.CurrentUICulture); + private String getDateTime(DateTime dt) + { + return dt.ToString("g", CultureInfo.CurrentUICulture); } - String concatTags(List tags) + private String concatTags(List tags) { StringBuilder sb = new StringBuilder(); foreach (string tag in tags) @@ -113,70 +448,98 @@ namespace keepass2android sb.Append(", "); } if (tags.Count > 0) - sb.Remove(sb.Length-2,2); + sb.Remove(sb.Length - 2, 2); return sb.ToString(); } - void PopulateExtraStrings(bool trimList) + private void PopulateExtraStrings() { - ViewGroup extraGroup = (ViewGroup)FindViewById(Resource.Id.extra_strings); - if (trimList) + ViewGroup extraGroup = (ViewGroup) FindViewById(Resource.Id.extra_strings); + foreach (var pair in Entry.Strings.Where(pair => !PwDefs.IsStandardField(pair.Key)).OrderBy(pair => pair.Key)) { - extraGroup.RemoveAllViews(); + var stringView = CreateExtraSection(pair.Key, pair.Value.ReadString(), pair.Value.IsProtected); + extraGroup.AddView(stringView.View); } - bool hasExtraFields = false; - foreach (var view in from pair in new Dictionary() { { "Field header", "field value" }, { "another header", "_aiaeiae" } } - orderby pair.Key - select CreateEditSection(pair.Key, pair.Value, true)) - { - extraGroup.AddView(view); - hasExtraFields = true; - } - FindViewById(Resource.Id.entry_extra_strings_label).Visibility = hasExtraFields ? ViewStates.Visible : ViewStates.Gone; + } - View CreateEditSection(string key, string value, bool isProtected) + private ExtraStringView CreateExtraSection(string key, string value, bool isProtected) { LinearLayout layout = new LinearLayout(this, null) {Orientation = Orientation.Vertical}; - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent); - layoutParams.SetMargins(10,0,0,0); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, + ViewGroup.LayoutParams.WrapContent); + layout.LayoutParameters = layoutParams; - View viewInflated = LayoutInflater.Inflate(Resource.Layout.entry_extrastring_title,null); - TextView keyView = (TextView)viewInflated; + View viewInflated = LayoutInflater.Inflate(Resource.Layout.entry_extrastring_title, null); + TextView keyView = viewInflated.FindViewById(Resource.Id.entry_title); if (key != null) keyView.Text = key; - - layout.AddView(keyView); - TextView valueView = (TextView)LayoutInflater.Inflate(Resource.Layout.entry_extrastring_value, null); + + layout.AddView(viewInflated); + RelativeLayout valueViewContainer = + (RelativeLayout) LayoutInflater.Inflate(Resource.Layout.entry_extrastring_value, null); + var valueView = valueViewContainer.FindViewById(Resource.Id.entry_extra); if (value != null) valueView.Text = value; SetPasswordTypeface(valueView); if (isProtected) + { RegisterProtectedTextView(valueView); + valueView.TransformationMethod = PasswordTransformationMethod.Instance; + } + layout.AddView(valueViewContainer); + var stringView = new ExtraStringView(layout, valueView, keyView); + + _stringViews.Add(key, stringView); + RegisterTextPopup(valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots), key, isProtected); + + return stringView; - if ((int)Build.VERSION.SdkInt >= 11) - valueView.SetTextIsSelectable(true); - layout.AddView(valueView); - return layout; } + private List RegisterPopup(string popupKey, View clickView, View anchorView) + { + clickView.Click += (sender, args) => + { + ShowPopup(anchorView, popupKey); + }; + _popupMenuItems[popupKey] = new List(); + return _popupMenuItems[popupKey]; + } + + private void RegisterProtectedTextView(TextView protectedTextView) { _protectedTextViews.Add(protectedTextView); } - void PopulateBinaries(bool trimList) + private void PopulateBinaries() { - ViewGroup binariesGroup = (ViewGroup)FindViewById(Resource.Id.binaries); - if (trimList) - { - binariesGroup.RemoveAllViews(); - } - foreach (KeyValuePair pair in new Dictionary() { {"abc",""}, {"test.png","uia"} }) + ViewGroup binariesGroup = (ViewGroup) FindViewById(Resource.Id.binaries); + foreach (KeyValuePair pair in new Dictionary() {{"abc", ""}, {"test.png", "uia"}}) { String key = pair.Key; + + + RelativeLayout valueViewContainer = + (RelativeLayout) LayoutInflater.Inflate(Resource.Layout.entry_extrastring_value, null); + var valueView = valueViewContainer.FindViewById(Resource.Id.entry_extra); + if (key != null) + valueView.Text = key; + + string popupKey = Strings.PrefixBinary + key; + + var itemList = RegisterPopup(popupKey, valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots)); + itemList.Add(new WriteBinaryToFilePopupItem(key, this)); + itemList.Add(new OpenBinaryPopupItem(key, this)); + + + + + binariesGroup.AddView(valueViewContainer); + /* Button binaryButton = new Button(this); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.FillParent, ViewGroup.LayoutParams.WrapContent); binaryButton.Text = key; @@ -206,8 +569,8 @@ namespace keepass2android }; binariesGroup.AddView(binaryButton,layoutParams); + */ - } FindViewById(Resource.Id.entry_binaries_label).Visibility = true ? ViewStates.Visible : ViewStates.Gone; } @@ -217,7 +580,8 @@ namespace keepass2android { String type = null; String extension = MimeTypeMap.GetFileExtensionFromUrl(url); - if (extension != null) { + if (extension != null) + { MimeTypeMap mime = MimeTypeMap.Singleton; type = mime.GetMimeTypeFromExtension(extension); } @@ -229,94 +593,182 @@ namespace keepass2android base.OnBackPressed(); } - protected void FillData(bool trimList) + protected void FillData() { _protectedTextViews = new List(); - ImageView iv = (ImageView)FindViewById(Resource.Id.entry_icon); + ImageView iv = (ImageView) FindViewById(Resource.Id.entry_icon); if (iv != null) { iv.SetImageDrawable(Resources.GetDrawable(Resource.Drawable.ic00)); } - - + + ActionBar.Title = "Entry title"; ActionBar.SetDisplayHomeAsUpEnabled(true); - - PopulateText(Resource.Id.entry_user_name, Resource.Id.entry_user_name_label, "user name"); - - PopulateText(Resource.Id.entry_url, Resource.Id.entry_url_label, "www.google.com"); - PopulateText(Resource.Id.entry_password, Resource.Id.entry_password_label, "my password"); + + + + PopulateStandardText(Resource.Id.entry_user_name, Resource.Id.entryfield_container_username, PwDefs.UserNameField); + PopulateStandardText(Resource.Id.entry_url, Resource.Id.entryfield_container_url, PwDefs.UrlField); + PopulateStandardText(Resource.Id.entry_password, Resource.Id.entryfield_container_password, PwDefs.PasswordField); RegisterProtectedTextView(FindViewById(Resource.Id.entry_password)); SetPasswordTypeface(FindViewById(Resource.Id.entry_password)); - - - PopulateText(Resource.Id.entry_created, Resource.Id.entry_created_label, getDateTime(DateTime.Now)); - PopulateText(Resource.Id.entry_modified, Resource.Id.entry_modified_label, getDateTime(DateTime.Now)); - - if (true) - { - FindViewById(Resource.Id.entry_expires).Visibility = ViewStates.Visible; - FindViewById(Resource.Id.entry_expires_label).Visibility = ViewStates.Visible; - - PopulateText(Resource.Id.entry_expires, Resource.Id.entry_expires_label, getDateTime(DateTime.Now)); - } + RegisterTextPopup(FindViewById(Resource.Id.username_container), + FindViewById(Resource.Id.username_vdots), PwDefs.UserNameField); + RegisterTextPopup(FindViewById(Resource.Id.url_container), + FindViewById(Resource.Id.url_vdots), PwDefs.UrlField) + .Add(new GotoUrlMenuItem(this)); + RegisterTextPopup(FindViewById(Resource.Id.password_container), + FindViewById(Resource.Id.password_vdots), PwDefs.PasswordField); + + + PopulateText(Resource.Id.entry_created, Resource.Id.entryfield_container_created, getDateTime(Entry.CreationTime)); + PopulateText(Resource.Id.entry_modified, Resource.Id.entryfield_container_modified, getDateTime(Entry.LastModificationTime)); + + if (Entry.Expires) + { + PopulateText(Resource.Id.entry_expires, Resource.Id.entryfield_container_expires, getDateTime(Entry.ExpiryTime)); + + } else { - FindViewById(Resource.Id.entry_expires).Visibility = ViewStates.Gone; - FindViewById(Resource.Id.entry_expires_label).Visibility = ViewStates.Gone; + PopulateText(Resource.Id.entry_expires, Resource.Id.entryfield_container_expires, null); } - PopulateText(Resource.Id.entry_comment, Resource.Id.entry_comment_label, "some text about this entry"); + PopulateStandardText(Resource.Id.entry_comment, Resource.Id.entryfield_container_comment, PwDefs.NotesField); + PopulateText(Resource.Id.entry_tags, Resource.Id.entryfield_container_tags, concatTags(Entry.Tags)); + PopulateText(Resource.Id.entry_override_url, Resource.Id.entryfield_container_overrideurl, Entry.OverrideUrl); - PopulateText(Resource.Id.entry_tags, Resource.Id.entry_tags_label, "bla; blubb; blablubb"); + PopulateExtraStrings(); - PopulateExtraStrings(trimList); - - PopulateBinaries(trimList); + PopulateBinaries(); SetPasswordStyle(); } + protected override void OnActivityResult(int requestCode, Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + if (resultCode == /*TODO*/ 0) + { + if (resultCode == /*TODO*/ 0) + { + RequiresRefresh(); + } + Recreate(); + } + } + + protected override void OnDestroy() + { + NotifyPluginsOnClose(); + base.OnDestroy(); + } + + private void NotifyPluginsOnClose() + { + Intent i = new Intent(Strings.ActionCloseEntryView); + i.PutExtra(Strings.ExtraSender, PackageName); + foreach (var plugin in new PluginDatabase(this).GetPluginsWithAcceptedScope(Strings.ScopeCurrentEntry)) + { + i.SetPackage(plugin); + SendBroadcast(i); + } + } + private List RegisterTextPopup(View container, View anchor, string fieldKey) + { + return RegisterTextPopup(container, anchor, fieldKey, Entry.Strings.GetSafe(fieldKey).IsProtected); + } + + private List RegisterTextPopup(View container, View anchor, string fieldKey, bool isProtected) + { + string popupKey = Strings.PrefixString + fieldKey; + var popupItems = RegisterPopup( + popupKey, + container, + anchor); + popupItems.Add(new CopyToClipboardPopupMenuIcon(this, _stringViews[fieldKey])); + if (isProtected) + popupItems.Add(new ToggleVisibilityPopupMenuItem(this)); + return popupItems; + } + + + + private void ShowPopup(View anchor, string popupKey) + { + //PopupMenu popupMenu = new PopupMenu(this, FindViewById(Resource.Id.entry_user_name)); + PopupMenu popupMenu = new PopupMenu(this, anchor); + + AccessManager.PreparePopup(popupMenu); + int itemId = 0; + foreach (IPopupMenuItem popupItem in _popupMenuItems[popupKey]) + { + popupMenu.Menu.Add(0, itemId, 0, popupItem.Text) + .SetIcon(popupItem.Icon); + itemId++; + } + + popupMenu.MenuItemClick += delegate(object sender, PopupMenu.MenuItemClickEventArgs args) + { + _popupMenuItems[popupKey][args.Item.ItemId].HandleClick(); + }; + popupMenu.Show(); + } + + private void ShowPopup(int resAnchor, string popupKey) + { + ShowPopup(FindViewById(resAnchor), popupKey); + } + private void SetPasswordTypeface(TextView textView) { - + } - private void PopulateText(int viewId, int headerViewId,int resId) { - View header = FindViewById(headerViewId); - TextView tv = (TextView)FindViewById(viewId); + private void PopulateText(int viewId, int containerViewId, int resId) + { + View header = FindViewById(containerViewId); + TextView tv = (TextView) FindViewById(viewId); header.Visibility = tv.Visibility = ViewStates.Visible; - tv.SetText (resId); + tv.SetText(resId); } - - private void PopulateText(int viewId, int headerViewId, String text) + + private void PopulateText(int viewId, int containerViewId, String text) { - View header = FindViewById(headerViewId); - TextView tv = (TextView)FindViewById(viewId); + View container = FindViewById(containerViewId); + TextView tv = (TextView) FindViewById(viewId); if (String.IsNullOrEmpty(text)) { - header.Visibility = tv.Visibility = ViewStates.Gone; + container.Visibility = tv.Visibility = ViewStates.Gone; } else { - header.Visibility = tv.Visibility = ViewStates.Visible; + container.Visibility = tv.Visibility = ViewStates.Visible; tv.Text = text; } } - void RequiresRefresh () + private void PopulateStandardText(int viewId, int containerViewId, String key) { - Intent ret = new Intent (); - ret.PutExtra (KeyRefreshPos, _pos); - + PopulateText(viewId, containerViewId, Entry.Strings.ReadSafe(key)); + _stringViews.Add(key, new StandardStringView(viewId, containerViewId, this)); } - - - - private void SetPasswordStyle() { + + private void RequiresRefresh() + { + Intent ret = new Intent(); + ret.PutExtra(KeyRefreshPos, _pos); + + } + + + + private void SetPasswordStyle() + { foreach (TextView password in _protectedTextViews) { @@ -330,12 +782,13 @@ namespace keepass2android } } } + protected override void OnResume() { - + base.OnResume(); } - + /// /// brings up a dialog asking the user whether he wants to add the given URL to the entry for automatic finding /// @@ -344,24 +797,50 @@ namespace keepass2android AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.SetTitle(GetString(Resource.String.AddUrlToEntryDialog_title)); - builder.SetMessage(GetString(Resource.String.AddUrlToEntryDialog_text, new Java.Lang.Object[] { url } )); + builder.SetMessage(GetString(Resource.String.AddUrlToEntryDialog_text, new Java.Lang.Object[] {url})); builder.SetPositiveButton(GetString(Resource.String.yes), (dlgSender, dlgEvt) => { - + }); builder.SetNegativeButton(GetString(Resource.String.no), (dlgSender, dlgEvt) => - { - CompleteOnCreate(); - }); + { + CompleteOnCreate(); + }); Dialog dialog = builder.Create(); dialog.Show(); } + public void ToggleVisibility() + { + _showPassword = !_showPassword; + SetPasswordStyle(); + UpdateTogglePasswordMenu(); + } + + public Android.Net.Uri WriteBinaryToFile(string key, bool writeToCacheDirectory) + { + return Android.Net.Uri.Empty; + //TODO + } + + private void UpdateTogglePasswordMenu() + { + //todo use real method + } + + public void GotoUrl() + { + //TODO + + } + + public void OpenBinaryFile(Uri newUri) + { + Toast.MakeText(this, "opening file TODO", ToastLength.Short).Show(); + } } - -} - +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/ExtraStringView.cs b/src/PluginHostTest/EntryActivityClasses/ExtraStringView.cs new file mode 100644 index 00000000..65edfb48 --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/ExtraStringView.cs @@ -0,0 +1,44 @@ +using System; +using Android.Views; +using Android.Widget; + +namespace keepass2android +{ + internal class ExtraStringView : IStringView + { + private readonly View _container; + private readonly TextView _valueView; + private readonly TextView _keyView; + + public ExtraStringView(LinearLayout container, TextView valueView, TextView keyView) + { + _container = container; + _valueView = valueView; + _keyView = keyView; + } + + public View View + { + get { return _container; } + } + + public string Text + { + get { return _valueView.Text; } + set + { + if (String.IsNullOrEmpty(value)) + { + _valueView.Visibility = ViewStates.Gone; + _keyView.Visibility = ViewStates.Gone; + } + else + { + _valueView.Visibility = ViewStates.Visible; + _keyView.Visibility = ViewStates.Visible; + _valueView.Text = value; + } + } + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs new file mode 100644 index 00000000..c9a54f65 --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/IPopupMenuItem.cs @@ -0,0 +1,112 @@ +using System; +using Android.Content; +using Android.Graphics.Drawables; +using KeePassLib; +using PluginHostTest; + +namespace keepass2android +{ + internal interface IPopupMenuItem + { + Drawable Icon { get; } + String Text { get; } + + void HandleClick(); + } + + class GotoUrlMenuItem : IPopupMenuItem + { + private readonly EntryActivity _ctx; + + public GotoUrlMenuItem(EntryActivity ctx) + { + _ctx = ctx; + } + + public Drawable Icon + { + get { return _ctx.Resources.GetDrawable(Android.Resource.Drawable.IcMenuUpload); } + } + + public string Text + { + get { return _ctx.Resources.GetString(Resource.String.menu_url); } + } + + public void HandleClick() + { + //TODO + _ctx.GotoUrl(); + } + } + + class ToggleVisibilityPopupMenuItem : IPopupMenuItem + { + private readonly EntryActivity _activity; + + + public ToggleVisibilityPopupMenuItem(EntryActivity activity) + { + _activity = activity; + + } + + public Drawable Icon + { + get + { + //return new TextDrawable("\uF06E", _activity); + return _activity.Resources.GetDrawable(Resource.Drawable.ic_action_eye_open); + + } + } + public string Text + { + get + { + return _activity.Resources.GetString( + _activity._showPassword ? + Resource.String.menu_hide_password + : Resource.String.show_password); + } + } + + + public void HandleClick() + { + _activity.ToggleVisibility(); + } + } + + class CopyToClipboardPopupMenuIcon : IPopupMenuItem + { + private readonly Context _context; + private readonly IStringView _stringView; + + public CopyToClipboardPopupMenuIcon(Context context, IStringView stringView) + { + _context = context; + _stringView = stringView; + + } + + public Drawable Icon + { + get + { + return _context.Resources.GetDrawable(Resource.Drawable.ic_menu_copy_holo_light); + } + } + public string Text + { + //TODO localize + get { return "Copy to clipboard"; } + } + + + public void HandleClick() + { + CopyToClipboardService.CopyValueToClipboardWithTimeout(_context, _stringView.Text); + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/IStringView.cs b/src/PluginHostTest/EntryActivityClasses/IStringView.cs new file mode 100644 index 00000000..a5c5036c --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/IStringView.cs @@ -0,0 +1,7 @@ +namespace keepass2android +{ + internal interface IStringView + { + string Text { set; get; } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs b/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs new file mode 100644 index 00000000..1315e2fb --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/OpenBinaryPopupItem.cs @@ -0,0 +1,37 @@ +using Android.Graphics.Drawables; +using PluginHostTest; + +namespace keepass2android +{ + internal class OpenBinaryPopupItem : IPopupMenuItem + { + private readonly string _key; + private readonly EntryActivity _entryActivity; + + public OpenBinaryPopupItem(string key, EntryActivity entryActivity) + { + _key = key; + _entryActivity = entryActivity; + } + + public Drawable Icon + { + get { return _entryActivity.Resources.GetDrawable(Android.Resource.Drawable.IcMenuShare); } + } + + public string Text + { + get { return _entryActivity.Resources.GetString(Resource.String.SaveAttachmentDialog_open); } + } + + public void HandleClick() + { + Android.Net.Uri newUri = _entryActivity.WriteBinaryToFile(_key, true); + if (newUri != null) + { + _entryActivity.OpenBinaryFile(newUri); + } + + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/PluginMenuOption.cs b/src/PluginHostTest/EntryActivityClasses/PluginMenuOption.cs new file mode 100644 index 00000000..d3e91ef4 --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/PluginMenuOption.cs @@ -0,0 +1,14 @@ +using Android.Content; +using Android.Graphics.Drawables; + +namespace keepass2android +{ + class PluginMenuOption + { + public string DisplayText { get; set; } + + public Drawable Icon { get; set; } + + public Intent Intent { get; set; } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs b/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs new file mode 100644 index 00000000..792f5bdd --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/PluginPopupMenuItem.cs @@ -0,0 +1,47 @@ +using Android.Content; +using Android.Graphics.Drawables; +using Android.OS; +using Keepass2android.Pluginsdk; + +namespace keepass2android +{ + class PluginPopupMenuItem : IPopupMenuItem + { + private readonly Context _ctx; + private readonly string _pluginPackage; + private readonly string _fieldId; + 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) + { + _ctx = ctx; + _pluginPackage = pluginPackage; + _fieldId = fieldId; + _displayText = displayText; + _iconId = iconId; + _bundleExtra = bundleExtra; + } + + public Drawable Icon + { + get { return _ctx.PackageManager.GetResourcesForApplication(_pluginPackage).GetDrawable(_iconId); } + } + public string Text + { + get { return _displayText; } + } + 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); + + _ctx.SendBroadcast(i); + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/StandardStringView.cs b/src/PluginHostTest/EntryActivityClasses/StandardStringView.cs new file mode 100644 index 00000000..bcbcb019 --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/StandardStringView.cs @@ -0,0 +1,44 @@ +using System; +using Android.App; +using Android.Views; +using Android.Widget; + +namespace keepass2android +{ + internal class StandardStringView : IStringView + { + private readonly int _viewId; + private readonly int _containerViewId; + private readonly Activity _activity; + + public StandardStringView(int viewId, int containerViewId, Activity activity) + { + _viewId = viewId; + _containerViewId = containerViewId; + _activity = activity; + } + + public string Text + { + set + { + View container = _activity.FindViewById(_containerViewId); + TextView tv = (TextView) _activity.FindViewById(_viewId); + if (String.IsNullOrEmpty(value)) + { + container.Visibility = tv.Visibility = ViewStates.Gone; + } + else + { + container.Visibility = tv.Visibility = ViewStates.Visible; + tv.Text = value; + } + } + get + { + TextView tv = (TextView) _activity.FindViewById(_viewId); + return tv.Text; + } + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs b/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs new file mode 100644 index 00000000..5d7f0c98 --- /dev/null +++ b/src/PluginHostTest/EntryActivityClasses/WriteBinaryToFilePopupItem.cs @@ -0,0 +1,32 @@ +using Android.Graphics.Drawables; +using PluginHostTest; + +namespace keepass2android +{ + internal class WriteBinaryToFilePopupItem : IPopupMenuItem + { + private readonly string _key; + private readonly EntryActivity _activity; + + public WriteBinaryToFilePopupItem(string key, EntryActivity activity) + { + _key = key; + _activity = activity; + } + + public Drawable Icon + { + get { return _activity.Resources.GetDrawable(Android.Resource.Drawable.IcMenuSave); } + } + + public string Text + { + get { return _activity.Resources.GetString(Resource.String.SaveAttachmentDialog_save); } + } + + public void HandleClick() + { + _activity.WriteBinaryToFile(_key, false); + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/PluginArrayAdapter.cs b/src/PluginHostTest/PluginArrayAdapter.cs index c27ec331..5bdda435 100644 --- a/src/PluginHostTest/PluginArrayAdapter.cs +++ b/src/PluginHostTest/PluginArrayAdapter.cs @@ -1,3 +1,6 @@ +using Android.Content.PM; +using Android.Content.Res; +using Android.Graphics.Drawables; using Android.Widget; using Android.Content; using Android.Views; @@ -11,26 +14,32 @@ namespace keepass2android public class PluginItem { private readonly string _package; + private readonly Context _ctx; + private readonly Resources _pluginRes; - public PluginItem(string package, string _label, int _icon, string _version, string _enabledStatus) + public PluginItem(string package, string enabledStatus, Context ctx) { _package = package; - Label = _label; - Icon = _icon; - Version = _version; - EnabledStatus = _enabledStatus; + _ctx = ctx; + EnabledStatus = enabledStatus; + _pluginRes = _ctx.PackageManager.GetResourcesForApplication(_package); } public string Label { - get; - set; + get + { + return PluginDetailsActivity.GetStringFromPlugin(_pluginRes, _package, "kp2aplugin_title"); + } + } public string Version { - get; - set; + get + { + return _ctx.PackageManager.GetPackageInfo(_package, 0).VersionName; + } } public string EnabledStatus @@ -39,10 +48,12 @@ namespace keepass2android set; } - public int Icon + public Drawable Icon { - get; - set; + get + { + return _ctx.PackageManager.GetApplicationIcon(_package); + } } public string Package @@ -104,7 +115,7 @@ namespace keepass2android holder.txtTitle.Text = item.Label; holder.txtVersion.Text = item.Version; holder.txtEnabledStatus.Text = item.EnabledStatus; - holder.imgIcon.SetImageResource(item.Icon); + holder.imgIcon.SetImageDrawable(item.Icon); return row; } diff --git a/src/PluginHostTest/PluginDatabase.cs b/src/PluginHostTest/PluginDatabase.cs index a38cdb0a..9a54c5fe 100644 --- a/src/PluginHostTest/PluginDatabase.cs +++ b/src/PluginHostTest/PluginDatabase.cs @@ -125,6 +125,18 @@ namespace keepass2android public bool IsValidAccessToken(string pluginPackage, string accessToken, string scope) { + if (pluginPackage == null) + { + Log.Warn(_tag, "No pluginPackage specified!"); + return false; + } + + if (accessToken == null) + { + Log.Warn(_tag, "No accessToken specified!"); + return false; + } + var prefs = GetPreferencesForPlugin(pluginPackage); if (prefs.GetString(_accessToken, null) != accessToken) { diff --git a/src/PluginHostTest/PluginDetailsActivity.cs b/src/PluginHostTest/PluginDetailsActivity.cs index 2fb4fd58..812e76ff 100644 --- a/src/PluginHostTest/PluginDetailsActivity.cs +++ b/src/PluginHostTest/PluginDetailsActivity.cs @@ -96,7 +96,7 @@ namespace PluginHostTest FindViewById(resourceId).Visibility = ViewStates.Gone; } - private static string GetStringFromPlugin(Resources pluginRes, string pluginPackage, string stringId) + public static string GetStringFromPlugin(Resources pluginRes, string pluginPackage, string stringId) { int titleId = pluginRes.GetIdentifier(pluginPackage + ":string/"+stringId, null, null); string title = null; diff --git a/src/PluginHostTest/PluginHost.cs b/src/PluginHostTest/PluginHost.cs index ca33b2b3..b867313d 100644 --- a/src/PluginHostTest/PluginHost.cs +++ b/src/PluginHostTest/PluginHost.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; - +using System.Xml.Linq; using Android.App; using Android.Content; using Android.Content.PM; @@ -11,9 +12,12 @@ using Android.Runtime; using Android.Util; using Android.Views; using Android.Widget; - +using KeePassLib; +using KeePassLib.Serialization; +using KeePassLib.Utility; using Keepass2android; using Keepass2android.Pluginsdk; +using Org.Json; using PluginHostTest; namespace keepass2android @@ -26,7 +30,7 @@ namespace keepass2android private const string _tag = "KP2A_PluginHost"; - private static readonly string[] _validScopes = { Strings.ScopeDatabaseActions }; + private static readonly string[] _validScopes = { Strings.ScopeDatabaseActions, Strings.ScopeCurrentEntry }; public static void TriggerRequests(Context ctx) { @@ -138,6 +142,34 @@ namespace keepass2android return true; } - + public static void AddEntryToIntent(Intent intent, PwEntry entry) + { + /*//add the entry XML + not yet implemented. What to do with attachments? + MemoryStream memStream = new MemoryStream(); + KdbxFile.WriteEntries(memStream, new[] {entry}); + string entryData = StrUtil.Utf8.GetString(memStream.ToArray()); + intent.PutExtra(Strings.ExtraEntryData, entryData); + */ + //add the compiled string array (placeholders replaced taking into account the db context) + Dictionary compiledFields = new Dictionary(); + foreach (var pair in entry.Strings) + { + String key = pair.Key; + + 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); + var jsonStr = json.ToString(); + intent.PutExtra(Strings.ExtraCompiledEntryData, jsonStr); + + intent.PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString()); + + } } } \ No newline at end of file diff --git a/src/PluginHostTest/PluginHostTest.csproj b/src/PluginHostTest/PluginHostTest.csproj index bc6de219..dca03023 100644 --- a/src/PluginHostTest/PluginHostTest.csproj +++ b/src/PluginHostTest/PluginHostTest.csproj @@ -56,24 +56,38 @@ + + + + + + + + + + + + + + - - + + @@ -93,6 +107,10 @@ + + {545B4A6B-8BBA-4FBE-92FC-4AC060122A54} + KeePassLib2Android + {3da3911e-36de-465e-8f15-f1991b6437e5} PluginSdkBinding @@ -567,6 +585,18 @@ + + + + + + + + + + + + - + + - - - - - - - - - - + + + - - - - - - - - - - - + + + + - - + + + + + + + + + + + + - - - + + + + + + + + - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/PluginHostTest/Resources/Menu/entry.xml b/src/PluginHostTest/Resources/Menu/entry.xml new file mode 100644 index 00000000..85137873 --- /dev/null +++ b/src/PluginHostTest/Resources/Menu/entry.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/src/PluginHostTest/Resources/Values-v14/styles.xml b/src/PluginHostTest/Resources/Values-v14/styles.xml index c8315ddc..6efa5214 100644 --- a/src/PluginHostTest/Resources/Values-v14/styles.xml +++ b/src/PluginHostTest/Resources/Values-v14/styles.xml @@ -54,7 +54,7 @@ 12dip 4dp ?android:attr/textColorSecondary - 18sp + 16sp diff --git a/src/PluginHostTest/Resources/Values/Strings2.xml b/src/PluginHostTest/Resources/Values/Strings2.xml index 5b8cf766..191a316b 100644 --- a/src/PluginHostTest/Resources/Values/Strings2.xml +++ b/src/PluginHostTest/Resources/Values/Strings2.xml @@ -8,6 +8,6 @@ Plugin will be notified when a database is opened, closed or saved. Current entry data - Plugin will receive all data about the current database entry and will be allowed to offer actions on it. + Plugin will receive all data about the current database entry and will be allowed to offer actions and modify the display of it. diff --git a/src/PluginHostTest/SprCompileFlags.cs b/src/PluginHostTest/SprCompileFlags.cs new file mode 100644 index 00000000..f7888cd5 --- /dev/null +++ b/src/PluginHostTest/SprCompileFlags.cs @@ -0,0 +1,7 @@ +namespace keepass2android +{ + public enum SprCompileFlags + { + All + } +} \ No newline at end of file diff --git a/src/PluginHostTest/SprContext.cs b/src/PluginHostTest/SprContext.cs new file mode 100644 index 00000000..ce644bff --- /dev/null +++ b/src/PluginHostTest/SprContext.cs @@ -0,0 +1,13 @@ +using System; +using KeePassLib; + +namespace keepass2android +{ + public class SprContext + { + public SprContext(PwEntry entry, PwDatabase kpDatabase, object all) + { + + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/SprEngine.cs b/src/PluginHostTest/SprEngine.cs new file mode 100644 index 00000000..e5503d7e --- /dev/null +++ b/src/PluginHostTest/SprEngine.cs @@ -0,0 +1,11 @@ +namespace keepass2android +{ + public class SprEngine + + { + public static string Compile(string value, SprContext sprContext) + { + return value; + } + } +} \ No newline at end of file diff --git a/src/PluginHostTest/TextDrawable.cs b/src/PluginHostTest/TextDrawable.cs new file mode 100644 index 00000000..26bc242e --- /dev/null +++ b/src/PluginHostTest/TextDrawable.cs @@ -0,0 +1,56 @@ +using System; +using Android.Content; +using Android.Graphics; +using Android.Graphics.Drawables; + +namespace keepass2android +{ + /// + /// Shows text as a drawable. + /// + /// Based on http://stackoverflow.com/questions/3972445/how-to-put-text-in-a-drawable + public class TextDrawable: Drawable { + + private readonly String _text; + private readonly Paint _paint; + private static Typeface _iconFont; + + public TextDrawable(String text, Context ctx) { + + _text = text; + + + if (_iconFont == null) + _iconFont = Typeface.CreateFromAsset(ctx.Assets, "fontawesome-webfont.ttf"); + + _paint = new Paint {Color = (Color.White), TextSize = 22f, AntiAlias = true}; + //_paint.SetTypeface(_iconFont); + _paint.SetShadowLayer(6f, 0, 0, Color.Black); + _paint.SetStyle(Paint.Style.Fill); + _paint.TextAlign = Paint.Align.Left; + } + + + public override void Draw(Canvas canvas) { + canvas.DrawText("x"+_text, 0, 0, _paint); + } + + + public override void SetAlpha(int alpha) { + _paint.Alpha = alpha; + } + + + public override void SetColorFilter(ColorFilter cf) + { + _paint.SetColorFilter(cf); + } + + public override int Opacity + { + get { return -3; /*translucent*/ } + } + + + } +} \ No newline at end of file diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/AccessManager.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/AccessManager.java index e4f94ad3..c43d3031 100644 --- a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/AccessManager.java +++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/AccessManager.java @@ -1,5 +1,7 @@ package keepass2android.pluginsdk; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import org.json.JSONArray; @@ -10,10 +12,15 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.PopupMenu; public class AccessManager { + private static final String _tag = "Kp2aPluginSDK"; private static final String PREF_KEY_SCOPE = "scope"; private static final String PREF_KEY_TOKEN = "token"; @@ -49,19 +56,40 @@ public class AccessManager public static void storeAccessToken(Context ctx, String hostPackage, String accessToken, ArrayList scopes) { SharedPreferences prefs = getPrefsForHost(ctx, hostPackage); - - // - if (accessToken.equals(prefs.getString(PREF_KEY_TOKEN, ""))) - { - //token already available - return; - } - + Editor edit = prefs.edit(); edit.putString(PREF_KEY_TOKEN, accessToken); - edit.putString(PREF_KEY_SCOPE, stringArrayToString(scopes)); + String scopesString = stringArrayToString(scopes); + edit.putString(PREF_KEY_SCOPE, scopesString); edit.commit(); + Log.d(_tag, "stored access token " + accessToken.substring(0, 4)+"... for "+scopes.size()+" scopes ("+scopesString+")."); + + } + + public static void preparePopup(Object popupMenu) + { + try + { + Field[] fields = popupMenu.getClass().getDeclaredFields(); + for (Field field : fields) { + if ("mPopup".equals(field.getName())) { + field.setAccessible(true); + Object menuPopupHelper = field.get(popupMenu); + Class classPopupHelper = Class.forName(menuPopupHelper + .getClass().getName()); + Method setForceIcons = classPopupHelper.getMethod( + "setForceShowIcon", boolean.class); + setForceIcons.invoke(menuPopupHelper, true); + break; + } + } + + } + catch (Exception e) + { + e.printStackTrace(); + } } private static SharedPreferences getPrefsForHost(Context ctx, @@ -72,14 +100,22 @@ public class AccessManager public static String tryGetAccessToken(Context ctx, String hostPackage, ArrayList scopes) { + if (TextUtils.isEmpty(hostPackage)) + { + Log.d(_tag, "hostPackage is empty!"); + return null; + } SharedPreferences prefs = getPrefsForHost(ctx, hostPackage); - ArrayList currentScope = stringToStringArray(prefs.getString(PREF_KEY_SCOPE, "")); + String scopesString = prefs.getString(PREF_KEY_SCOPE, ""); + Log.d(_tag, "scopes: "+ scopesString); + ArrayList currentScope = stringToStringArray(scopesString); if (isSubset(scopes, currentScope)) { return prefs.getString(PREF_KEY_TOKEN, null); } else { + Log.d(_tag, "looks like scope changed. Access token invalid."); return null; } } @@ -88,7 +124,10 @@ public class AccessManager ArrayList availableScopes) { for (String r: requiredScopes){ if (availableScopes.indexOf(r)<0) + { + Log.d(_tag, "Scope "+r+" not available. "+availableScopes.size()); return false; + } } return true; } @@ -106,4 +145,15 @@ public class AccessManager } } + + /** + * Returns a valid access token or throws PluginAccessException + */ + public static String getAccessToken(Context context, String hostPackage, + ArrayList scopes) throws PluginAccessException { + String accessToken = tryGetAccessToken(context, hostPackage, scopes); + if (accessToken == null) + throw new PluginAccessException(hostPackage, scopes); + return accessToken; + } } diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessBroadcastReceiver.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessBroadcastReceiver.java index bfa8370f..ca8d0123 100644 --- a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessBroadcastReceiver.java +++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/PluginAccessBroadcastReceiver.java @@ -34,6 +34,8 @@ public abstract class PluginAccessBroadcastReceiver extends BroadcastReceiver { public void onReceive(Context ctx, Intent intent) { String action = intent.getAction(); android.util.Log.d("KP2A.pluginsdk", "received broadcast with action="+action); + if (action == null) + return; if (action.equals(Strings.ACTION_TRIGGER_REQUEST_ACCESS)) { requestAccess(ctx, intent); diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java index 2ea28c46..5ba3de3a 100644 --- a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java +++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java @@ -1,19 +1,114 @@ package keepass2android.pluginsdk; public class Strings { + /** + * Plugin is notified about actions like open/close/update a database. + */ public static final String SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"; + /** + * Plugin is notified when an entry is opened. + */ public static final String SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"; + /** + * Extra key to transfer a (json serialized) list of scopes + */ public static final String EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"; + + /** + * Extra key for sending the package name of the sender of a broadcast. + * Should be set in every broadcast. + */ public static final String EXTRA_SENDER = "keepass2android.EXTRA_SENDER"; + + /** + * Extra key for sending a request token. The request token is passed from + * KP2A to the plugin. It's used in the authorization process. + */ public static final String EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"; + /** + * Action sent from KP2A to the plugin to indicate that the plugin should request + * access (sending it's scopes) + */ public static final String ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"; + /** + * Action sent from the plugin to KP2A including the scopes. + */ public static final String ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"; + /** + * Action sent from the KP2A to the plugin when the user grants access. + * Will contain an access token. + */ public static final String ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"; + /** + * Action sent from KP2A to the plugin to indicate that access is not or no longer valid. + */ public static final String ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"; - - public static final String EXTRA_ACCESS_TOKEN = "EXTRA_ACCESS_TOKEN"; - //static final String SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"; + /** + * Action sent from KP2A to the plugin to indicate that an entry was opened. + * The Intent contains the full entry data. + */ + public static final String ACTION_OPEN_ENTRY= "keepass2android.ACTION_OPEN_ENTRY"; + + /** + * Action sent from KP2A to the plugin to indicate that an entry activity was closed. + */ + public static final String ACTION_CLOSE_ENTRY_VIEW= "keepass2android.ACTION_CLOSE_ENTRY_VIEW"; + + /** + * Extra key for a string containing the GUID of the entry. + */ + public static final String EXTRA_ENTRY_ID= "keepass2android.EXTRA_ENTRY_DATA"; + + /** + * Json serialized data of the PwEntry (C# class) representing the opened entry. + * currently not implemented. + */ + //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) + */ + public static final String EXTRA_COMPILED_ENTRY_DATA = "keepass2android.EXTRA_COMPILED_ENTRY_DATA"; + + /** + * Extra key for passing the access token (both ways) + */ + public static final String EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"; + + /** + * Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry. + * Requires SCOPE_CURRENT_ENTRY. + */ + public static final String ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"; + + public static final String EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"; + 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"; + + /** 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"; + + /** + * Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user. + * + */ + public static final String ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"; + + /** + * Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry. + * May be used to update existing or add new fields at any time while the entry is opened. + */ + public static final String ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"; + + public static final String EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"; + public static final String EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"; + + public static final String PREFIX_STRING = "STRING_"; + public static final String PREFIX_BINARY = "BINARY_"; + + }