rename back to current names
This commit is contained in:
133
src/keepass2android-app/pluginhost/PluginArrayAdapter.cs
Normal file
133
src/keepass2android-app/pluginhost/PluginArrayAdapter.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using Android.Content.PM;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.Widget;
|
||||
using Android.Content;
|
||||
using Android.Views;
|
||||
using System.Collections.Generic;
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents information about a plugin for display in the plugin list activity
|
||||
/// </summary>
|
||||
public class PluginItem
|
||||
{
|
||||
private readonly string _package;
|
||||
private readonly Context _ctx;
|
||||
private readonly Resources _pluginRes;
|
||||
|
||||
public PluginItem(string package, string enabledStatus, Context ctx)
|
||||
{
|
||||
_package = package;
|
||||
_ctx = ctx;
|
||||
EnabledStatus = enabledStatus;
|
||||
_pluginRes = _ctx.PackageManager.GetResourcesForApplication(_package);
|
||||
}
|
||||
|
||||
public string Label
|
||||
{
|
||||
get
|
||||
{
|
||||
return PluginDetailsActivity.GetStringFromPlugin(_pluginRes, _package, "kp2aplugin_title");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
return _ctx.PackageManager.GetPackageInfo(_package, 0).VersionName;
|
||||
}
|
||||
}
|
||||
|
||||
public string EnabledStatus
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Drawable Icon
|
||||
{
|
||||
get
|
||||
{
|
||||
return _ctx.PackageManager.GetApplicationIcon(_package);
|
||||
}
|
||||
}
|
||||
|
||||
public string Package
|
||||
{
|
||||
get { return _package; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class PluginArrayAdapter : ArrayAdapter<PluginItem>
|
||||
{
|
||||
|
||||
class PluginViewHolder : Java.Lang.Object
|
||||
{
|
||||
public ImageView imgIcon;
|
||||
public TextView txtTitle;
|
||||
public TextView txtVersion;
|
||||
|
||||
public TextView txtEnabledStatus;
|
||||
}
|
||||
|
||||
Context context;
|
||||
int layoutResourceId;
|
||||
IList<PluginItem> data = null;
|
||||
|
||||
public PluginArrayAdapter(IntPtr javaReference, JniHandleOwnership transfer)
|
||||
: base(javaReference, transfer)
|
||||
{
|
||||
}
|
||||
|
||||
public PluginArrayAdapter(Context context, int layoutResourceId, IList<PluginItem> data) :
|
||||
base(context, layoutResourceId, data)
|
||||
{
|
||||
|
||||
this.layoutResourceId = layoutResourceId;
|
||||
this.context = context;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public override View GetView(int position, View convertView, ViewGroup parent)
|
||||
{
|
||||
View row = convertView;
|
||||
PluginViewHolder holder = null;
|
||||
|
||||
if (row == null)
|
||||
{
|
||||
LayoutInflater inflater = ((Activity)context).LayoutInflater;
|
||||
row = inflater.Inflate(layoutResourceId, parent, false);
|
||||
|
||||
holder = new PluginViewHolder();
|
||||
holder.imgIcon = (ImageView)row.FindViewById(Resource.Id.imgIcon);
|
||||
holder.txtTitle = (TextView)row.FindViewById(Resource.Id.txtLabel);
|
||||
holder.txtVersion = (TextView)row.FindViewById(Resource.Id.txtVersion);
|
||||
holder.txtEnabledStatus = (TextView)row.FindViewById(Resource.Id.txtStatus);
|
||||
|
||||
row.Tag = holder;
|
||||
}
|
||||
else
|
||||
{
|
||||
holder = (PluginViewHolder)row.Tag;
|
||||
}
|
||||
|
||||
var item = data[position];
|
||||
holder.txtTitle.Text = item.Label;
|
||||
holder.txtVersion.Text = item.Version;
|
||||
holder.txtEnabledStatus.Text = item.EnabledStatus;
|
||||
holder.imgIcon.SetImageDrawable(item.Icon);
|
||||
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
242
src/keepass2android-app/pluginhost/PluginDatabase.cs
Normal file
242
src/keepass2android-app/pluginhost/PluginDatabase.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Util;
|
||||
using Keepass2android.Pluginsdk;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
public class PluginDatabase
|
||||
{
|
||||
public class KeyGenerator
|
||||
{
|
||||
public static string GetUniqueKey(int maxSize)
|
||||
{
|
||||
char[] chars =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
|
||||
byte[] data = new byte[1];
|
||||
RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();
|
||||
crypto.GetNonZeroBytes(data);
|
||||
data = new byte[maxSize];
|
||||
crypto.GetNonZeroBytes(data);
|
||||
StringBuilder result = new StringBuilder(maxSize);
|
||||
foreach (byte b in data)
|
||||
{
|
||||
result.Append(chars[b % (chars.Length)]);
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private const string _tag = "KP2A_PluginDatabase";
|
||||
private readonly Context _ctx;
|
||||
private const string _accessToken = "accessToken";
|
||||
private const string _scopes = "scopes";
|
||||
private const string _requesttoken = "requestToken";
|
||||
|
||||
|
||||
|
||||
public PluginDatabase(Context ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
}
|
||||
|
||||
private ISharedPreferences GetPreferencesForPlugin(string packageName)
|
||||
{
|
||||
var prefs = _ctx.GetSharedPreferences("KP2A.Plugin." + packageName, FileCreationMode.Private);
|
||||
if (prefs.GetString(_requesttoken, null) == null)
|
||||
{
|
||||
var editor = prefs.Edit();
|
||||
editor.PutString(_requesttoken, Guid.NewGuid().ToString());
|
||||
editor.Commit();
|
||||
}
|
||||
|
||||
return prefs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the request token for the plugin. Request token is created of not yet available.
|
||||
/// </summary>
|
||||
/// <returns>Request token. Never null or empty.</returns>
|
||||
public string GetRequestToken(string pkgName)
|
||||
{
|
||||
return GetPreferencesForPlugin(pkgName).GetString(_requesttoken, null);
|
||||
}
|
||||
|
||||
public IList<string> GetPluginScopes(string pluginPackage)
|
||||
{
|
||||
var prefs = GetPreferencesForPlugin(pluginPackage);
|
||||
return AccessManager.StringToStringArray(prefs.GetString(_scopes, ""));
|
||||
}
|
||||
|
||||
public IEnumerable<String> GetAllPluginPackages()
|
||||
{
|
||||
return PluginHost.GetAllPlugins(_ctx);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return GetPreferencesForPlugin(pluginPackage).GetString(_accessToken, null) != null;
|
||||
}
|
||||
|
||||
|
||||
public void StorePlugin(string pluginPackage, string accessToken, IList<string> requestedScopes)
|
||||
{
|
||||
ISharedPreferences pluginPrefs = GetPreferencesForPlugin(pluginPackage);
|
||||
|
||||
pluginPrefs.Edit()
|
||||
.PutString(_scopes, AccessManager.StringArrayToString(requestedScopes))
|
||||
.PutString(_accessToken, accessToken)
|
||||
.Commit();
|
||||
}
|
||||
|
||||
public void SetEnabled(string pluginPackage, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
string accessToken = KeyGenerator.GetUniqueKey(32);
|
||||
|
||||
Intent i = new Intent(Strings.ActionReceiveAccess);
|
||||
i.SetPackage(pluginPackage);
|
||||
|
||||
i.PutExtra(Strings.ExtraSender, _ctx.PackageName);
|
||||
i.PutExtra(Strings.ExtraRequestToken, GetPreferencesForPlugin(pluginPackage).GetString(_requesttoken, null));
|
||||
i.PutExtra(Strings.ExtraAccessToken, accessToken);
|
||||
_ctx.SendBroadcast(i);
|
||||
|
||||
StorePlugin(pluginPackage, accessToken, GetPluginScopes(pluginPackage));
|
||||
}
|
||||
else
|
||||
{
|
||||
Intent i = new Intent(Strings.ActionRevokeAccess);
|
||||
i.SetPackage(pluginPackage);
|
||||
i.PutExtra(Strings.ExtraSender, _ctx.PackageName);
|
||||
i.PutExtra(Strings.ExtraRequestToken, GetPreferencesForPlugin(pluginPackage).GetString(_requesttoken, null));
|
||||
_ctx.SendBroadcast(i);
|
||||
StorePlugin(pluginPackage, null, GetPluginScopes(pluginPackage));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//internal access token is valid for all scopes
|
||||
if ((pluginPackage == _ctx.PackageName) && (accessToken == GetInternalToken()))
|
||||
return true;
|
||||
|
||||
var prefs = GetPreferencesForPlugin(pluginPackage);
|
||||
if (prefs.GetString(_accessToken, null) != accessToken)
|
||||
{
|
||||
Log.Warn(_tag, "Invalid access token for " + pluginPackage);
|
||||
return false;
|
||||
}
|
||||
if (!AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(scope))
|
||||
{
|
||||
Log.Warn(_tag, "Scope " + scope + " not granted for " + pluginPackage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetAccessToken(string pluginPackage)
|
||||
{
|
||||
return GetPreferencesForPlugin(pluginPackage).GetString(_accessToken, null);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (string plugin in GetAllPluginPackages())
|
||||
{
|
||||
GetPreferencesForPlugin(plugin).Edit().Clear().Commit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<string> GetPluginsWithAcceptedScope(string scope)
|
||||
{
|
||||
return GetAllPluginPackages().Where(plugin =>
|
||||
{
|
||||
var prefs = GetPreferencesForPlugin(plugin);
|
||||
return (prefs.GetString(_accessToken, null) != null)
|
||||
&& AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(scope);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void ClearPlugin(string plugin)
|
||||
{
|
||||
var prefs = _ctx.GetSharedPreferences("KP2A.Plugin." + plugin, FileCreationMode.Private);
|
||||
prefs.Edit().Clear().Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given pluginPackage has been granted the requiredScope
|
||||
/// </summary>
|
||||
public bool HasAcceptedScope(string pluginPackage, string requiredScope)
|
||||
{
|
||||
if (pluginPackage == null)
|
||||
{
|
||||
Log.Warn(_tag, "No pluginPackage specified!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var prefs = GetPreferencesForPlugin(pluginPackage);
|
||||
if (prefs.GetString(_accessToken, null) == null)
|
||||
{
|
||||
Log.Info(_tag, "No access token for " + pluginPackage);
|
||||
return false;
|
||||
}
|
||||
if (!AccessManager.StringToStringArray(prefs.GetString(_scopes, "")).Contains(requiredScope))
|
||||
{
|
||||
Log.Info(_tag, "Scope " + requiredScope + " not granted for " + pluginPackage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public string GetInternalToken()
|
||||
{
|
||||
var prefs = _ctx.GetSharedPreferences("KP2A.PluginHost" , FileCreationMode.Private);
|
||||
if (prefs.Contains(_accessToken))
|
||||
{
|
||||
return prefs.GetString(_accessToken, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = KeyGenerator.GetUniqueKey(32);
|
||||
prefs.Edit().PutString(_accessToken, token).Commit();
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
159
src/keepass2android-app/pluginhost/PluginDetailsActivity.cs
Normal file
159
src/keepass2android-app/pluginhost/PluginDetailsActivity.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Content.Res;
|
||||
using Android.Graphics.Drawables;
|
||||
using Android.OS;
|
||||
using Android.Runtime;
|
||||
using Android.Util;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using Java.Util;
|
||||
using Keepass2android.Pluginsdk;
|
||||
using keepass2android.views;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = AppNames.AppName, Theme = "@style/android:Theme.Material.Light", Exported = true)]
|
||||
[IntentFilter(new[] { Strings.ActionEditPluginSettings },
|
||||
Label = AppNames.AppName,
|
||||
Categories = new[] { Intent.CategoryDefault })]
|
||||
public class PluginDetailsActivity : Activity
|
||||
{
|
||||
private CheckBox _checkbox;
|
||||
private string _pluginPackageName;
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
new ActivityDesign(this).ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
|
||||
_pluginPackageName = Intent.GetStringExtra(Strings.ExtraPluginPackage);
|
||||
|
||||
var pluginRes = PackageManager.GetResourcesForApplication(_pluginPackageName);
|
||||
var title = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_title");
|
||||
var author = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_author");
|
||||
var shortDesc = GetStringFromPlugin(pluginRes, _pluginPackageName, "kp2aplugin_shortdesc");
|
||||
var version = PackageManager.GetPackageInfo(_pluginPackageName, 0).VersionName;
|
||||
|
||||
SetContentView(Resource.Layout.plugin_details);
|
||||
if (title != null)
|
||||
FindViewById<TextView>(Resource.Id.txtLabel).Text = title;
|
||||
FindViewById<TextView>(Resource.Id.txtVersion).Text = version;
|
||||
SetTextOrHide(Resource.Id.txtAuthor, author);
|
||||
SetTextOrHide(Resource.Id.txtShortDesc, shortDesc);
|
||||
|
||||
_checkbox = FindViewById<CheckBox>(Resource.Id.cb_enabled);
|
||||
_checkbox.CheckedChange += delegate
|
||||
{
|
||||
new PluginDatabase(this).SetEnabled(_pluginPackageName, _checkbox.Checked);
|
||||
};
|
||||
|
||||
Drawable d = PackageManager.GetApplicationIcon(_pluginPackageName);
|
||||
FindViewById<ImageView>(Resource.Id.imgIcon).SetImageDrawable(d);
|
||||
|
||||
FindViewById<TextView>(Resource.Id.txtVersion).Text = version;
|
||||
|
||||
//cannot be wrong to update the view when we received an update
|
||||
PluginHost.OnReceivedRequest += OnPluginHostOnOnReceivedRequest;
|
||||
|
||||
if (Intent.Action == Strings.ActionEditPluginSettings)
|
||||
{
|
||||
//this action can be triggered by external apps so we don't know if anybody has ever triggered
|
||||
//the plugin to request access. Do this now, don't set the view right now
|
||||
PluginHost.TriggerRequest(this, _pluginPackageName, new PluginDatabase(this));
|
||||
//show the buttons instead of the checkbox
|
||||
_checkbox.Visibility = ViewStates.Invisible;
|
||||
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Invisible; //show them only after access is requested
|
||||
FindViewById(Resource.Id.deny_button).Visibility = ViewStates.Visible;
|
||||
|
||||
FindViewById(Resource.Id.accept_button).Click += delegate(object sender, EventArgs args)
|
||||
{
|
||||
new PluginDatabase(this).SetEnabled(_pluginPackageName, true);
|
||||
SetResult(Result.Ok);
|
||||
Finish();
|
||||
};
|
||||
|
||||
FindViewById(Resource.Id.deny_button).Click += delegate(object sender, EventArgs args)
|
||||
{
|
||||
new PluginDatabase(this).SetEnabled(_pluginPackageName, false);
|
||||
SetResult(Result.Canceled);
|
||||
Finish();
|
||||
};
|
||||
|
||||
//in case the plugin requested scopes previously, make sure we display them
|
||||
UpdateView();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateView();
|
||||
_checkbox.Visibility = ViewStates.Visible;
|
||||
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Gone;
|
||||
FindViewById(Resource.Id.deny_button).Visibility = ViewStates.Gone;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPluginHostOnOnReceivedRequest(object sender, PluginHost.PluginHostEventArgs args)
|
||||
{
|
||||
if (args.Package == _pluginPackageName)
|
||||
{
|
||||
FindViewById(Resource.Id.accept_button).Visibility = ViewStates.Visible;
|
||||
UpdateView();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
PluginHost.OnReceivedRequest -= OnPluginHostOnOnReceivedRequest;
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
private void UpdateView()
|
||||
{
|
||||
var scopesContainer = FindViewById<LinearLayout>(Resource.Id.scopes_list);
|
||||
scopesContainer.RemoveAllViews();
|
||||
|
||||
var pluginDb = new PluginDatabase(this);
|
||||
_checkbox.Checked = pluginDb.IsEnabled(_pluginPackageName);
|
||||
foreach (string scope in pluginDb.GetPluginScopes(_pluginPackageName))
|
||||
{
|
||||
string scopeId = scope.Substring("keepass2android.".Length);
|
||||
|
||||
TextWithHelp help = new TextWithHelp(this,
|
||||
GetString(Resources.GetIdentifier(scopeId + "_title", "string", PackageName)),
|
||||
GetString(Resources.GetIdentifier(scopeId + "_explanation", "string", PackageName)));
|
||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FillParent,
|
||||
ViewGroup.LayoutParams.WrapContent);
|
||||
help.LayoutParameters = layoutParams;
|
||||
scopesContainer.AddView(help);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTextOrHide(int resourceId, string text)
|
||||
{
|
||||
if (text != null)
|
||||
{
|
||||
FindViewById<TextView>(resourceId).Text = text;
|
||||
FindViewById<TextView>(resourceId).Visibility = ViewStates.Visible;
|
||||
}
|
||||
else
|
||||
FindViewById<TextView>(resourceId).Visibility = ViewStates.Gone;
|
||||
}
|
||||
|
||||
public static string GetStringFromPlugin(Resources pluginRes, string pluginPackage, string stringId)
|
||||
{
|
||||
int titleId = pluginRes.GetIdentifier(pluginPackage + ":string/" + stringId, null, null);
|
||||
string title = null;
|
||||
if (titleId != 0)
|
||||
title = pluginRes.GetString(titleId);
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
184
src/keepass2android-app/pluginhost/PluginHost.cs
Normal file
184
src/keepass2android-app/pluginhost/PluginHost.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Util;
|
||||
using Group.Pals.Android.Lib.UI.Filechooser.Utils;
|
||||
using KeePassLib.Utility;
|
||||
using Keepass2android.Pluginsdk;
|
||||
using Org.Json;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
/// <summary>
|
||||
/// Class which manages plugins inside the app
|
||||
/// </summary>
|
||||
[BroadcastReceiver(Exported = true)]
|
||||
[IntentFilter(new[] { Strings.ActionRequestAccess })]
|
||||
public class PluginHost : BroadcastReceiver
|
||||
{
|
||||
|
||||
private const string _tag = "KP2A_PluginHost";
|
||||
|
||||
|
||||
private static readonly string[] _validScopes = { Strings.ScopeDatabaseActions,
|
||||
Strings.ScopeCurrentEntry,
|
||||
Strings.ScopeQueryCredentials,
|
||||
Strings.ScopeQueryCredentialsForOwnPackage};
|
||||
|
||||
public static IEnumerable<string> GetAllPlugins(Context ctx)
|
||||
{
|
||||
Intent accessIntent = new Intent(Strings.ActionTriggerRequestAccess);
|
||||
PackageManager packageManager = ctx.PackageManager;
|
||||
IList<ResolveInfo> dictPacks = packageManager.QueryBroadcastReceivers(
|
||||
accessIntent, PackageInfoFlags.Receivers);
|
||||
|
||||
return dictPacks.Select(ri => ri.ActivityInfo.ApplicationInfo).Select(appInfo => appInfo.PackageName);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a broadcast to all potential plugins prompting them to request access to our app.
|
||||
/// </summary>
|
||||
public static void TriggerRequests(Context ctx)
|
||||
{
|
||||
PluginDatabase pluginDatabase = new PluginDatabase(ctx);
|
||||
foreach (string pkg in GetAllPlugins(ctx))
|
||||
TriggerRequest(ctx, pkg, pluginDatabase);
|
||||
|
||||
}
|
||||
|
||||
public static void TriggerRequest(Context ctx, string pkgName, PluginDatabase pluginDatabase)
|
||||
{
|
||||
try
|
||||
{
|
||||
Intent triggerIntent = new Intent(Strings.ActionTriggerRequestAccess);
|
||||
triggerIntent.SetPackage(pkgName);
|
||||
triggerIntent.PutExtra(Strings.ExtraSender, ctx.PackageName);
|
||||
string requestToken = pluginDatabase.GetRequestToken(pkgName);
|
||||
triggerIntent.PutExtra(Strings.ExtraRequestToken, requestToken);
|
||||
Android.Util.Log.Debug(_tag, "Request token: " + requestToken);
|
||||
ctx.SendBroadcast(triggerIntent);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void OnReceive(Context context, Intent intent)
|
||||
{
|
||||
PluginDatabase pluginDb = new PluginDatabase(context);
|
||||
if (intent.Action == Strings.ActionRequestAccess)
|
||||
{
|
||||
string senderPackage = intent.GetStringExtra(Strings.ExtraSender);
|
||||
string requestToken = intent.GetStringExtra(Strings.ExtraRequestToken);
|
||||
|
||||
IList<string> requestedScopes = intent.GetStringArrayListExtra(Strings.ExtraScopes);
|
||||
|
||||
if (!AreScopesValid(requestedScopes))
|
||||
{
|
||||
Log.Debug(_tag, "requested scopes not valid");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pluginDb.GetRequestToken(senderPackage) != requestToken)
|
||||
{
|
||||
Log.Warn(_tag, "Invalid requestToken!");
|
||||
return;
|
||||
}
|
||||
string currentAccessToken = pluginDb.GetAccessToken(senderPackage);
|
||||
if ((currentAccessToken != null)
|
||||
&& (AccessManager.IsSubset(requestedScopes,
|
||||
pluginDb.GetPluginScopes(senderPackage))))
|
||||
{
|
||||
//permission already there.
|
||||
var i = new Intent(Strings.ActionReceiveAccess);
|
||||
i.PutExtra(Strings.ExtraSender, context.PackageName);
|
||||
i.PutExtra(Strings.ExtraAccessToken, currentAccessToken);
|
||||
//TODO: Plugin should verify requestToken to make sure it doesn't receive accessTokens from malicious apps
|
||||
i.PutExtra(Strings.ExtraRequestToken, requestToken);
|
||||
i.SetPackage(senderPackage);
|
||||
context.SendBroadcast(i);
|
||||
|
||||
Log.Debug(_tag, "Plugin " + senderPackage + " enabled.");
|
||||
}
|
||||
else
|
||||
{
|
||||
//store that scope was requested but not yet approved (=> accessToken = null)
|
||||
pluginDb.StorePlugin(senderPackage, null, requestedScopes);
|
||||
|
||||
Log.Debug(_tag, "Plugin " + senderPackage + " not enabled.");
|
||||
|
||||
//see if the plugin has an access token
|
||||
string accessToken = intent.GetStringExtra(Strings.ExtraAccessToken);
|
||||
if (accessToken != null)
|
||||
{
|
||||
//notify plugin that access token is no longer valid or sufficient
|
||||
Intent i = new Intent(Strings.ActionRevokeAccess);
|
||||
i.PutExtra(Strings.ExtraSender, context.PackageName);
|
||||
i.PutExtra(Strings.ExtraAccessToken, accessToken);
|
||||
i.SetPackage(senderPackage);
|
||||
context.SendBroadcast(i);
|
||||
Log.Warn(_tag, "Access token of plugin " + senderPackage + " not (or no more) valid.");
|
||||
}
|
||||
|
||||
}
|
||||
if (OnReceivedRequest != null)
|
||||
OnReceivedRequest(this, new PluginHostEventArgs() { Package = senderPackage });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private bool AreScopesValid(IList<string> requestedScopes)
|
||||
{
|
||||
foreach (string scope in requestedScopes)
|
||||
{
|
||||
if (!_validScopes.Contains(scope))
|
||||
{
|
||||
Log.Warn(_tag, "invalid scope: " + scope);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// adds the entry output data to the intent to be sent to a plugin
|
||||
/// </summary>
|
||||
public static void AddEntryToIntent(Intent intent, PwEntryOutput 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 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());
|
||||
|
||||
JSONObject jsonOutput = new JSONObject(outputFields);
|
||||
var jsonOutputStr = jsonOutput.ToString();
|
||||
intent.PutExtra(Strings.ExtraEntryOutputData, jsonOutputStr);
|
||||
|
||||
JSONArray jsonProtectedFields = new JSONArray(
|
||||
(System.Collections.ICollection)entry.OutputStrings
|
||||
.Where(pair => pair.Value.IsProtected)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray());
|
||||
intent.PutExtra(Strings.ExtraProtectedFieldsList, jsonProtectedFields.ToString());
|
||||
|
||||
intent.PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString());
|
||||
|
||||
}
|
||||
|
||||
public class PluginHostEventArgs
|
||||
{
|
||||
public string Package { get; set; }
|
||||
}
|
||||
public static event EventHandler<PluginHostEventArgs> OnReceivedRequest;
|
||||
}
|
||||
}
|
72
src/keepass2android-app/pluginhost/PluginListActivity.cs
Normal file
72
src/keepass2android-app/pluginhost/PluginListActivity.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Widget;
|
||||
using Keepass2android.Pluginsdk;
|
||||
using keepass2android;
|
||||
|
||||
namespace keepass2android
|
||||
{
|
||||
[Activity(Label = "@string/plugins", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, Theme="@style/android:Theme.Material.Light", Exported = true)]
|
||||
[IntentFilter(new[] { "kp2a.action.PluginListActivity" }, Categories = new[] { Intent.CategoryDefault })]
|
||||
public class PluginListActivity : ListActivity
|
||||
{
|
||||
private PluginArrayAdapter _pluginArrayAdapter;
|
||||
private List<PluginItem> _items;
|
||||
|
||||
protected override void OnCreate(Bundle bundle)
|
||||
{
|
||||
new ActivityDesign(this).ApplyTheme();
|
||||
base.OnCreate(bundle);
|
||||
|
||||
|
||||
|
||||
SetContentView(Resource.Layout.plugin_list);
|
||||
|
||||
PluginHost.TriggerRequests(this);
|
||||
|
||||
ListView listView = FindViewById<ListView>(Android.Resource.Id.List);
|
||||
listView.ItemClick +=
|
||||
(sender, args) =>
|
||||
{
|
||||
Intent i = new Intent(this, typeof(PluginDetailsActivity));
|
||||
i.PutExtra(Strings.ExtraPluginPackage, _items[args.Position].Package);
|
||||
StartActivity(i);
|
||||
};
|
||||
|
||||
FindViewById<Button>(Resource.Id.btnPluginsOnline).Click += delegate
|
||||
{
|
||||
Util.GotoUrl(this, "https://github.com/PhilippC/keepass2android/blob/master/docs/Available-Plug-ins.md");
|
||||
};
|
||||
|
||||
}
|
||||
protected override void OnResume()
|
||||
{
|
||||
|
||||
PluginDatabase pluginDb = new PluginDatabase(this);
|
||||
|
||||
_items = (from pluginPackage in pluginDb.GetAllPluginPackages()
|
||||
let version = PackageManager.GetPackageInfo(pluginPackage, 0).VersionName
|
||||
let enabledStatus = pluginDb.IsEnabled(pluginPackage) ? GetString(Resource.String.plugin_enabled) : GetString(Resource.String.plugin_disabled)
|
||||
select new PluginItem(pluginPackage, enabledStatus, this)).ToList();
|
||||
/*
|
||||
{
|
||||
new PluginItem("PluginA", Resource.Drawable.Icon, "keepass2android.plugina", "connected"),
|
||||
new PluginItem("KeepassNFC", Resource.Drawable.Icon, "com.bla.blubb.plugina", "disconnected")
|
||||
};
|
||||
* */
|
||||
_pluginArrayAdapter = new PluginArrayAdapter(this, Resource.Layout.ListViewPluginRow, _items);
|
||||
ListAdapter = _pluginArrayAdapter;
|
||||
base.OnResume();
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
ListAdapter = _pluginArrayAdapter = null;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user