diff --git a/src/AutoFillPlugin/AccessReceiver.cs b/src/AutoFillPlugin/AccessReceiver.cs new file mode 100644 index 00000000..a8182f73 --- /dev/null +++ b/src/AutoFillPlugin/AccessReceiver.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using Keepass2android.Pluginsdk; + +namespace keepass2android.AutoFillPlugin +{ + [BroadcastReceiver(Exported = true)] + [IntentFilter(new[] { Strings.ActionTriggerRequestAccess, Strings.ActionReceiveAccess, Strings.ActionRevokeAccess })] + public class AccessReceiver : PluginAccessBroadcastReceiver + { + public override void OnReceive(Context context, Intent intent) + { + Android.Util.Log.Debug("KP2AAS", intent.Action); + base.OnReceive(context, intent); + } + + public override IList Scopes + { + get + { + return new List + { + Strings.ScopeQueryCredentials + }; + } + } + } +} \ No newline at end of file diff --git a/src/AutoFillPlugin/Assets/AboutAssets.txt b/src/AutoFillPlugin/Assets/AboutAssets.txt new file mode 100644 index 00000000..ee398862 --- /dev/null +++ b/src/AutoFillPlugin/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with you package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/src/AutoFillPlugin/AutoFillPlugin.csproj b/src/AutoFillPlugin/AutoFillPlugin.csproj new file mode 100644 index 00000000..93850d6e --- /dev/null +++ b/src/AutoFillPlugin/AutoFillPlugin.csproj @@ -0,0 +1,105 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {6FF440E6-E8FF-4E43-8221-9E3972F14812} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + keepass2android.AutoFillPlugin + AutoFillPlugin + 512 + true + Resources\Resource.Designer.cs + Off + True + v5.0 + Properties\AndroidManifest.xml + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + True + None + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + False + SdkOnly + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + {545b4a6b-8bba-4fbe-92fc-4ac060122a54} + KeePassLib2Android + + + {3da3911e-36de-465e-8f15-f1991b6437e5} + PluginSdkBinding + + + + + + + + \ No newline at end of file diff --git a/src/AutoFillPlugin/Credentials.cs b/src/AutoFillPlugin/Credentials.cs new file mode 100644 index 00000000..8f09f772 --- /dev/null +++ b/src/AutoFillPlugin/Credentials.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +namespace keepass2android.AutoFillPlugin +{ + public class Credentials + { + public string User; + public string Password; + public string Url; + } +} \ No newline at end of file diff --git a/src/AutoFillPlugin/GettingStarted.Xamarin b/src/AutoFillPlugin/GettingStarted.Xamarin new file mode 100644 index 00000000..e9d4f6a4 --- /dev/null +++ b/src/AutoFillPlugin/GettingStarted.Xamarin @@ -0,0 +1,4 @@ + + GS\Android\CS\AndroidApp\GettingStarted.html + false + \ No newline at end of file diff --git a/src/AutoFillPlugin/Kp2aAccessibilityService.cs b/src/AutoFillPlugin/Kp2aAccessibilityService.cs new file mode 100644 index 00000000..52b981d9 --- /dev/null +++ b/src/AutoFillPlugin/Kp2aAccessibilityService.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Views.Accessibility; +using Android.Widget; + + +namespace keepass2android.AutoFillPlugin +{ + // + [Service(Enabled =true, Permission= "android.permission.BIND_ACCESSIBILITY_SERVICE")] + [IntentFilter(new[] { "android.accessibilityservice.AccessibilityService" })] + [MetaData("android.accessibilityservice", Resource = "@xml/accserviceconfig")] + public class Kp2aAccessibilityService : Android.AccessibilityServices.AccessibilityService, IDialogInterfaceOnCancelListener + { + const string _logTag = "KP2AAS"; + private const int autoFillNotificationId = 0; + private const string androidAppPrefix = "androidapp://"; + + public override void OnCreate() + { + base.OnCreate(); + Android.Util.Log.Debug(_logTag, "OnCreate Service"); + } + + protected override void OnServiceConnected() + { + Android.Util.Log.Debug(_logTag, "service connected"); + base.OnServiceConnected(); + } + + public override void OnAccessibilityEvent(AccessibilityEvent e) + { + + Android.Util.Log.Debug(_logTag, "OnAccEvent"); + bool cancelNotification = true; + if (e.EventType == EventTypes.WindowContentChanged || e.EventType == EventTypes.WindowStateChanged) + { + Android.Util.Log.Debug(_logTag, "event: " + e.EventType + ", package = " + e.PackageName); + var root = RootInActiveWindow; + if ((ExistsNodeOrChildren(root, n => n.WindowId == e.WindowId) && !ExistsNodeOrChildren(root, n => (n.ViewIdResourceName != null) && (n.ViewIdResourceName.StartsWith("com.android.systemui"))))) + { + var allEditTexts = GetNodeOrChildren(root, n=> { return IsEditText(n); }); + + var usernameEdit = allEditTexts.TakeWhile(edit => (edit.Password == false)).LastOrDefault(); + + string searchString = androidAppPrefix + root.PackageName; + + string url = androidAppPrefix + root.PackageName; + + if (root.PackageName == "com.android.chrome") + { + var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar").FirstOrDefault(); + UrlFromAddressField(ref url, addressField); + + } + else if (root.PackageName == "com.android.browser") + { + var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url").FirstOrDefault(); + UrlFromAddressField(ref url, addressField); + } + + var emptyPasswordFields = GetNodeOrChildren(root, n => { return IsPasswordField(n); }).ToList(); + if (emptyPasswordFields.Any()) + { + if ((LookupCredentialsActivity.LastReceivedCredentials != null) && (LookupCredentialsActivity.LastReceivedCredentials.Url == url)) + { + FillPassword(url, usernameEdit, emptyPasswordFields); + } + else + { + AskFillPassword(url, usernameEdit, emptyPasswordFields); + cancelNotification = false; + } + + } + + } + + } + if (cancelNotification) + ((NotificationManager)GetSystemService(NotificationService)).Cancel(autoFillNotificationId); + + } + private static void UrlFromAddressField(ref string url, AccessibilityNodeInfo addressField) + { + if (addressField != null) + { + url = addressField.Text; + if (!url.Contains("://")) + url = "http://" + url; + } + + } + + private static bool IsPasswordField(AccessibilityNodeInfo n) + { + //if (n.Password) Android.Util.Log.Debug(_logTag, "pwdx with " + (n.Text == null ? "null" : n.Text)); + var res = n.Password && string.IsNullOrEmpty(n.Text); + // if (n.Password) Android.Util.Log.Debug(_logTag, "pwd with " + n.Text + res); + return res; + } + + private static bool IsEditText(AccessibilityNodeInfo n) + { + //it seems like n.Editable is not a good check as this is false for some fields which are actually editable, at least in tests with Chrome. + return (n.ClassName != null) && (n.ClassName.Contains("EditText")); + } + + private void AskFillPassword(string url, AccessibilityNodeInfo usernameEdit, IEnumerable passwordFields) + { + var runSearchIntent = new Intent(this, typeof(LookupCredentialsActivity)); + runSearchIntent.PutExtra("url", url); + runSearchIntent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); + var pending = PendingIntent.GetActivity(this, 0, runSearchIntent, PendingIntentFlags.UpdateCurrent); + + var targetName = url; + + if (url.StartsWith(androidAppPrefix)) + { + var packageName = url.Substring(androidAppPrefix.Length); + try + { + targetName = PackageManager.GetPackageInfo(packageName, 0).ApplicationInfo.Name; + } + catch (Exception e) + { + Android.Util.Log.Debug(_logTag, e.ToString()); + targetName = packageName; + } + } + else + { + targetName = KeePassLib.Utility.UrlUtil.GetHost(url); + } + + + var builder = new Notification.Builder(this); + //TODO icon + //TODO plugin icon + builder.SetSmallIcon(Resource.Drawable.ic_notify_keyboard) + .SetContentText(GetString(Resource.String.NotificationContentText, new Java.Lang.Object[] { targetName })) + .SetContentTitle(GetString(Resource.String.NotificationTitle)) + .SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis()) + .SetTicker( GetString(Resource.String.NotificationTickerText, new Java.Lang.Object[] { targetName })) + .SetVisibility(Android.App.NotificationVisibility.Secret) + .SetContentIntent(pending); + var notificationManager = (NotificationManager)GetSystemService(NotificationService); + notificationManager.Notify(autoFillNotificationId, builder.Build()); + + } + + private void FillPassword(string url, AccessibilityNodeInfo usernameEdit, IEnumerable passwordFields) + { + + FillDataInTextField(usernameEdit, LookupCredentialsActivity.LastReceivedCredentials.User); + foreach (var pwd in passwordFields) + FillDataInTextField(pwd, LookupCredentialsActivity.LastReceivedCredentials.Password); + + LookupCredentialsActivity.LastReceivedCredentials = null; + } + + private static void FillDataInTextField(AccessibilityNodeInfo edit, string newValue) + { + Bundle b = new Bundle(); + b.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, newValue); + edit.PerformAction(Android.Views.Accessibility.Action.SetText, b); + } + + private bool ExistsNodeOrChildren(AccessibilityNodeInfo n, Func p) + { + return GetNodeOrChildren(n, p).Any(); + } + + private IEnumerable GetNodeOrChildren(AccessibilityNodeInfo n, Func p) + { + if (n != null) + { + if (p(n)) + yield return n; + for (int i = 0; i < n.ChildCount; i++) + { + foreach (var x in GetNodeOrChildren(n.GetChild(i), p)) + yield return x; + } + } + + } + + public override void OnInterrupt() + { + + } + + public void OnCancel(IDialogInterface dialog) + { + + } + } +} \ No newline at end of file diff --git a/src/AutoFillPlugin/LookupCredentialsActivity.cs b/src/AutoFillPlugin/LookupCredentialsActivity.cs new file mode 100644 index 00000000..9777df6e --- /dev/null +++ b/src/AutoFillPlugin/LookupCredentialsActivity.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +using Keepass2android.Pluginsdk; + +namespace keepass2android.AutoFillPlugin +{ + [Activity(Label = "@string/LookupTitle", LaunchMode = Android.Content.PM.LaunchMode.SingleInstance)] + public class LookupCredentialsActivity : Activity + { + protected override void OnCreate(Bundle bundle) + { + base.OnCreate(bundle); + + var url = Intent.GetStringExtra("url"); + _lastQueriedUrl = url; + StartActivityForResult(Kp2aControl.GetQueryEntryIntent(url), 123); + } + + string _lastQueriedUrl; + + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) + { + base.OnActivityResult(requestCode, resultCode, data); + + var jsonOutput = new Org.Json.JSONObject(data.GetStringExtra(Strings.ExtraEntryOutputData)); + Dictionary output = new Dictionary(); + for (var iter = jsonOutput.Keys(); iter.HasNext;) + { + string key = iter.Next().ToString(); + string value = jsonOutput.Get(key).ToString(); + output[key] = value; + } + + + string user = "", password = ""; + output.TryGetValue(KeePassLib.PwDefs.UserNameField, out user); + output.TryGetValue(KeePassLib.PwDefs.PasswordField, out password); + + LastReceivedCredentials = new Credentials() { User = user, Password = password, Url = _lastQueriedUrl }; + + Finish(); + } + + public static Credentials LastReceivedCredentials; + } +} \ No newline at end of file diff --git a/src/AutoFillPlugin/MainActivity.cs b/src/AutoFillPlugin/MainActivity.cs new file mode 100644 index 00000000..71c31ecc --- /dev/null +++ b/src/AutoFillPlugin/MainActivity.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +namespace keepass2android.AutoFillPlugin +{ + [Activity(Label = "MainActivity", MainLauncher =true)] + public class MainActivity : Activity + { + protected override void OnCreate(Bundle bundle) + { + base.OnCreate(bundle); + + // Create your application here + } + } +} \ No newline at end of file diff --git a/src/AutoFillPlugin/Properties/AndroidManifest.xml b/src/AutoFillPlugin/Properties/AndroidManifest.xml new file mode 100644 index 00000000..ef6e7cf6 --- /dev/null +++ b/src/AutoFillPlugin/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/AutoFillPlugin/Properties/AssemblyInfo.cs b/src/AutoFillPlugin/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a22aab22 --- /dev/null +++ b/src/AutoFillPlugin/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Android.App; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoFillPlugin")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoFillPlugin")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/AutoFillPlugin/Resources/AboutResources.txt b/src/AutoFillPlugin/Resources/AboutResources.txt new file mode 100644 index 00000000..194ae28a --- /dev/null +++ b/src/AutoFillPlugin/Resources/AboutResources.txt @@ -0,0 +1,50 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.xml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable-hdpi/ + icon.png + + drawable-ldpi/ + icon.png + + drawable-mdpi/ + icon.png + + layout/ + main.xml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called +"Resource" that contains the tokens for each one of the resources included. For example, +for the above Resources layout, this is what the Resource class would expose: + +public class Resource { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main +to reference the layout/main.xml file, or Resource.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/AutoFillPlugin/Resources/drawable-xhdpi/ic_notify_keyboard.png b/src/AutoFillPlugin/Resources/drawable-xhdpi/ic_notify_keyboard.png new file mode 100644 index 00000000..2e473aa2 Binary files /dev/null and b/src/AutoFillPlugin/Resources/drawable-xhdpi/ic_notify_keyboard.png differ diff --git a/src/AutoFillPlugin/Resources/drawable/Icon.png b/src/AutoFillPlugin/Resources/drawable/Icon.png new file mode 100644 index 00000000..8074c4c5 Binary files /dev/null and b/src/AutoFillPlugin/Resources/drawable/Icon.png differ diff --git a/src/AutoFillPlugin/Resources/values/Strings.xml b/src/AutoFillPlugin/Resources/values/Strings.xml new file mode 100644 index 00000000..d9e359e7 --- /dev/null +++ b/src/AutoFillPlugin/Resources/values/Strings.xml @@ -0,0 +1,8 @@ + + + Look up credentials + KP2A AutoFillPlugin + Keepass2Android AutoFill + AutoFill form for %1$s + AutoFill available for %1$s + diff --git a/src/AutoFillPlugin/Resources/xml/accserviceconfig.xml b/src/AutoFillPlugin/Resources/xml/accserviceconfig.xml new file mode 100644 index 00000000..5f9db482 --- /dev/null +++ b/src/AutoFillPlugin/Resources/xml/accserviceconfig.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/KeePass.sln b/src/KeePass.sln index 35046706..50351742 100644 --- a/src/KeePass.sln +++ b/src/KeePass.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.31101.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeePassLib2Android", "KeePassLib2Android\KeePassLib2Android.csproj", "{545B4A6B-8BBA-4FBE-92FC-4AC060122A54}" EndProject @@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZlibAndroid", "ZlibAndroid\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest2", "MaterialTest2\MaterialTest2.csproj", "{B7BBC4A2-0301-4DFF-B03C-C88CD4F1F890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoFillPlugin", "AutoFillPlugin\AutoFillPlugin.csproj", "{6FF440E6-E8FF-4E43-8221-9E3972F14812}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -408,6 +410,42 @@ Global {B7BBC4A2-0301-4DFF-B03C-C88CD4F1F890}.ReleaseNoNet|Mixed Platforms.Deploy.0 = Release|Any CPU {B7BBC4A2-0301-4DFF-B03C-C88CD4F1F890}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU {B7BBC4A2-0301-4DFF-B03C-C88CD4F1F890}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Mixed Platforms.Deploy.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Win32.ActiveCfg = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Win32.Build.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|Win32.Deploy.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|x64.ActiveCfg = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|x64.Build.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Debug|x64.Deploy.0 = Debug|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Any CPU.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Any CPU.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Mixed Platforms.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Win32.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Win32.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|Win32.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|x64.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|x64.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.Release|x64.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Any CPU.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Mixed Platforms.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|Win32.Deploy.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|x64.Build.0 = Release|Any CPU + {6FF440E6-E8FF-4E43-8221-9E3972F14812}.ReleaseNoNet|x64.Deploy.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE