Plugins: transferring list of protected fields

CreateDatabaseActivity: Passing app task to next activity
Keepass.cs: added documentation on Activities and AppTasks
SearchActivity.cs: passing appTask to next activity, using ForwardResult to pass ActivityResult back to previous activity
FileSelectActivity: pass AppTask to CreateDatabaseActivity, Recreate instead of Start+Finish (to have correct handling of ActivityResults)
This commit is contained in:
Philipp Crocoll 2014-05-14 07:23:31 +02:00
parent 00332523e6
commit f613206dab
19 changed files with 246 additions and 89 deletions

View File

@ -67,6 +67,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="TestIntentsAndBundles.cs" />
<Compile Include="ProgressDialogStub.cs" />
<Compile Include="TestBase.cs" />
<Compile Include="TestCacheSupervisor.cs" />
@ -85,6 +86,7 @@
<Compile Include="TestSynchronizeCachedDatabase.cs" />
</ItemGroup>
<ItemGroup>
<None Include="ClassDiagram1.cd" />
<None Include="Resources\AboutResources.txt" />
<None Include="Assets\AboutAssets.txt" />
</ItemGroup>

View File

@ -18,7 +18,8 @@ namespace Kp2aUnitTests
{
TestRunner runner = new TestRunner();
// Run all tests from this assembly
runner.AddTests(Assembly.GetExecutingAssembly());
//runner.AddTests(Assembly.GetExecutingAssembly());
runner.AddTests(new List<Type> { typeof(TestIntentsAndBundles) });
//runner.AddTests(new List<Type> { typeof(TestSynchronizeCachedDatabase)});
//runner.AddTests(typeof(TestLoadDb).GetMethod("LoadErrorWithCertificateTrustFailure"));
//runner.AddTests(typeof(TestLoadDb).GetMethod("LoadWithAcceptedCertificateTrustFailure"));

View File

@ -152,10 +152,15 @@ namespace keepass2android
//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());
//add field values as JSON ({ "key":"value", ... } form)
JSONObject json = new JSONObject(outputFields);
var jsonStr = json.ToString();
intent.PutExtra(Strings.ExtraEntryOutputData, jsonStr);
//add list of which fields are protected (StringArrayExtra)
string[] protectedFieldsList = entry.OutputStrings.Where(s=>s.Value.IsProtected).Select(s => s.Key).ToArray();
intent.PutExtra(Strings.ExtraProtectedFieldsList, protectedFieldsList);
intent.PutExtra(Strings.ExtraEntryId, entry.Uuid.ToHexString());
}

View File

@ -52,6 +52,12 @@ public abstract class PluginActionBroadcastReceiver extends BroadcastReceiver {
}
return res;
}
protected String[] getProtectedFieldsListFromIntent()
{
return _intent.getStringArrayExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST);
}
}
protected class ActionSelected extends PluginActionBase
@ -89,10 +95,23 @@ public abstract class PluginActionBroadcastReceiver extends BroadcastReceiver {
return getFieldId() == null;
}
/**
*
* @return a hashmap containing the entry fields in key/value form
*/
public HashMap<String, String> getEntryFields()
{
return getEntryFieldsFromIntent();
}
/**
*
* @return an array with the keys of all protected fields in the entry
*/
public String[] getProtectedFieldsList()
{
return getProtectedFieldsListFromIntent();
}
}
protected class CloseEntryView extends PluginActionBase
@ -125,6 +144,15 @@ public abstract class PluginActionBroadcastReceiver extends BroadcastReceiver {
return getEntryFieldsFromIntent();
}
/**
*
* @return an array with the keys of all protected fields in the entry
*/
public String[] getProtectedFieldsList()
{
return getProtectedFieldsListFromIntent();
}
public void addEntryAction(String actionDisplayText, int actionIconResourceId, Bundle actionData) throws PluginAccessException
{
addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData);

View File

@ -88,6 +88,12 @@ public class Strings {
*/
public static final String EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA";
/**
* Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
*/
public static final String EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST";
/**
* Extra key for passing the access token (both ways)
*/

View File

@ -27,7 +27,8 @@ namespace keepass2android
private bool _restoringInstanceState;
private bool _showPassword;
private ActivityDesign _design;
private readonly ActivityDesign _design;
private AppTask _appTask;
public CreateDatabaseActivity()
{
@ -57,7 +58,7 @@ namespace keepass2android
_design.ApplyTheme();
SetContentView(Resource.Layout.create_database);
_appTask = AppTask.GetTaskInOnCreate(bundle, Intent);
SetDefaultIoc();
@ -527,7 +528,7 @@ namespace keepass2android
dbHelper.CreateFile(_ioc, Filename);
}
GroupActivity.Launch(_activity, new NullTask());
GroupActivity.Launch(_activity, _activity._appTask);
_activity.Finish();
}

View File

@ -127,7 +127,9 @@ namespace keepass2android
public override bool OnSearchRequested()
{
StartActivityForResult(typeof(SearchActivity), 0);
Intent i = new Intent(this, typeof(SearchActivity));
AppTask.ToIntent(i);
StartActivityForResult(i, 0);
return true;
}
@ -214,35 +216,9 @@ namespace keepass2android
titleText = GetText(Resource.String.root);
}
//see if the button for SDK Version < 11 is there
Button tv = (Button)FindViewById(Resource.Id.group_name);
if (tv != null)
{
if (Group != null)
{
tv.Text = titleText;
}
if (clickable)
{
tv.Click += (sender, e) =>
{
AppTask.SetActivityResult(this, KeePass.ExitNormal);
Finish();
};
} else
{
tv.SetCompoundDrawables(null, null, null, null);
tv.Clickable = false;
}
}
//ICS?
if (Util.HasActionBar(this))
{
ActionBar.Title = titleText;
if (clickable)
ActionBar.SetDisplayHomeAsUpEnabled(true);
}
ActionBar.Title = titleText;
if (clickable)
ActionBar.SetDisplayHomeAsUpEnabled(true);
}
@ -256,6 +232,29 @@ namespace keepass2android
ActionBar.SetIcon(drawable);
}
}
class SuggestionListener: Java.Lang.Object, SearchView.IOnSuggestionListener
{
private readonly CursorAdapter _suggestionsAdapter;
public SuggestionListener(CursorAdapter suggestionsAdapter)
{
_suggestionsAdapter = suggestionsAdapter;
}
public bool OnSuggestionClick(int position)
{
var cursor = _suggestionsAdapter.Cursor;
cursor.MoveToPosition(position);
var x = cursor.GetString(cursor.GetColumnIndexOrThrow(SearchManager.SuggestColumnIntentDataId));
return true;
}
public bool OnSuggestionSelect(int position)
{
return false;
}
}
public override bool OnCreateOptionsMenu(IMenu menu) {
base.OnCreateOptionsMenu(menu);
@ -266,8 +265,9 @@ namespace keepass2android
{
var searchManager = (SearchManager) GetSystemService(Context.SearchService);
var searchView = (SearchView) menu.FindItem(Resource.Id.menu_search).ActionView;
searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName));
searchView.SetOnSuggestionListener(new SuggestionListener(searchView.SuggestionsAdapter));
}
var item = menu.FindItem(Resource.Id.menu_sync);
if (item != null)

View File

@ -28,6 +28,39 @@ using Java.Lang.Reflect;
using KeePassLib.Serialization;
using Exception = System.Exception;
using String = System.String;
/**
* General documentation
*
* Activity stack and activity results
* ===================================
*
* Keepass2Android comprises quite a number of different activities and entry points: The app can be started
* using the launcher icon (-> Activity "Keepass"), or by sending a URL (-> FileSelect), opening a .kdb(x)-file (->Password),
* swiping a YubikeyNEO (NfcOtpActivity).
* While the database is closed, there is only one activity on the stack: Keepass -> FileSelect <-> Password.
* After opening an database (in Password), Password is always the root of the stack (exception: after creating a database,
* FileSelect is the root without Password being open).
*
* Some possible stacks:
* Password -> Group ( -> Group (subgroups) ... ) -> EntryView -> EntryEdit
* (AdvancedSearch Menu) -> Search -> SearchResults -> EntryView -> EntryEdit
* (SearchWidget) -> SearchResults -> EntryView -> EntryEdit
* Password -> ShareUrlResults -> EntryView
*
*
* In each of these activities, an AppTask may be present and must be passed to started activities and ActivityResults
* must be returned. Therefore, if any Activity calls { StartActivity(newActivity);Finish(); }, it must specify FLAG_ACTIVITY_FORWARD_RESULT.
*
* Further sub-activities may be opened (e.g. Settings -> ExportDb, ...), but these are not necesarrily
* part of the AppTask. Then, neither the task has to be passed nor must the sub-activity return an ActivityResult.
*
* Activities with AppTasks should check if they get a new AppTask in OnActivityResult.
*
* Note: Chrome fires the ActionSend (Share URL) intent with NEW_TASK (i.e. KP2A appears in a separate task, either a new one,
* or, if it was running before, in the KP2A task), whereas Firefox doesn't specify that flag and KP2A appears "inside" Firefox.
* This means that the AppTask must be cleared for use in Chrome after finding an entry or pressing back button in ShareUrlResults.
* This would not be necessary for Firefox where the (Android) Task of standalone KP2A is not affected by the search.
*/
namespace keepass2android
{

View File

@ -115,10 +115,10 @@ namespace keepass2android
private class LockCloseActivityBroadcastReceiver : BroadcastReceiver
{
readonly LockCloseActivity _service;
public LockCloseActivityBroadcastReceiver(LockCloseActivity service)
readonly LockCloseActivity _activity;
public LockCloseActivityBroadcastReceiver(LockCloseActivity activity)
{
_service = service;
_activity = activity;
}
public override void OnReceive(Context context, Intent intent)
@ -126,7 +126,7 @@ namespace keepass2android
switch (intent.Action)
{
case Intents.DatabaseLocked:
_service.OnLockDatabase();
_activity.OnLockDatabase();
break;
case Intent.ActionScreenOff:
App.Kp2a.OnScreenOff();

View File

@ -95,10 +95,10 @@ namespace keepass2android
private class LockCloseListActivityBroadcastReceiver : BroadcastReceiver
{
readonly LockCloseListActivity _service;
public LockCloseListActivityBroadcastReceiver(LockCloseListActivity service)
readonly LockCloseListActivity _activity;
public LockCloseListActivityBroadcastReceiver(LockCloseListActivity activity)
{
_service = service;
_activity = activity;
}
public override void OnReceive(Context context, Intent intent)
@ -106,7 +106,7 @@ namespace keepass2android
switch (intent.Action)
{
case Intents.DatabaseLocked:
_service.OnLockDatabase();
_activity.OnLockDatabase();
break;
case Intent.ActionScreenOff:
App.Kp2a.OnScreenOff();

View File

@ -5831,7 +5831,10 @@ namespace keepass2android
public const int searchable = 2131034140;
// aapt resource value: 0x7f05001d
public const int searchable_offline = 2131034141;
public const int searchable_debug = 2131034141;
// aapt resource value: 0x7f05001e
public const int searchable_offline = 2131034142;
static Xml()
{

View File

@ -62,27 +62,12 @@ namespace keepass2android
SetResult(KeePass.ExitCloseAfterTaskComplete);
_db = App.Kp2a.GetDb();
String searchUrl = ((SearchUrlTask)AppTask).UrlToSearchFor;
if (!_db.Loaded)
if (App.Kp2a.DatabaseIsUnlocked)
{
Intent intent = new Intent(this, typeof(FileSelectActivity));
AppTask.ToIntent(intent);
intent.AddFlags(ActivityFlags.ClearTask | ActivityFlags.NewTask);
StartActivityForResult(intent, 0);
Finish();
}
else if (App.Kp2a.QuickLocked)
{
PasswordActivity.Launch(this,_db.Ioc, AppTask);
Finish();
}
else
{
Query(searchUrl);
String searchUrl = ((SearchUrlTask)AppTask).UrlToSearchFor;
Query(searchUrl);
}
// else: LockCloseListActivity.OnResume will trigger a broadcast (LockDatabase) which will cause the activity to be finished.
}
@ -99,8 +84,7 @@ namespace keepass2android
}
private void Query(String url)
{
{
try
{
//first: search for exact url

View File

@ -322,7 +322,13 @@ namespace keepass2android
((Spinner)dialog.FindViewById(Resource.Id.cred_remember_mode)).SetSelection((int)ioc.CredSaveMode);
}
public static void FinishAndForward(Activity activity, Intent i)
{
i.SetFlags(ActivityFlags.ForwardResult);
activity.StartActivity(i);
activity.Finish();
}
}
}

View File

@ -67,15 +67,17 @@ namespace keepass2android
#endif
public const int AppNameResource = Resource.String.app_name;
public const string AppNameShort = "@string/short_app_name";
public const string AppLauncherTitle = "@string/app_name";
public const string AppNameShort = "@string/short_app_name" + "DBG";
public const string AppLauncherTitle = "@string/app_name" + " Debug";
#if DEBUG
public const string PackagePart = "keepass2android_debug";
public const string Searchable = "@xml/searchable_debug";
#else
public const string PackagePart = "keepass2android";
public const string Searchable = "@xml/searchable";
#endif
public const int LauncherIcon = Resource.Drawable.ic_launcher;
public const string Searchable = "@xml/searchable";
}
#endif
/// <summary>

View File

@ -48,6 +48,29 @@ namespace keepass2android
#endregion
}
/// <summary>
/// represents data stored in an intent or bundle as extra string array
/// </summary>
public class StringArrayExtra : IExtra
{
public string Key { get; set; }
public string[] Value { get; set; }
#region IExtra implementation
public void ToBundle(Bundle b)
{
b.PutStringArray(Key, Value);
}
public void ToIntent(Intent i)
{
i.PutExtra(Key, Value);
}
#endregion
}
/// <summary>
/// base class for "tasks": these are things the user wants to do and which require several activities
/// </summary>
@ -271,8 +294,20 @@ namespace keepass2android
public override void AfterUnlockDatabase(PasswordActivity act)
{
ShareUrlResults.Launch(act, this);
RemoveTaskFromIntent(act);
act.AppTask = new NullTask();
//removed. this causes an issue in the following workflow:
//When the user wants to find an entry for a URL but has the wrong database open he needs
//to switch to another database. But the Task is removed already the first time when going through PasswordActivity
// (with the wrong db).
//Then after switching to the right database, the task is gone.
//A reason this code existed was the following workflow:
//Using Chrome browser (with NEW_TASK flag for ActionSend): Share URL -> KP2A.
//Now the AppTask was in PasswordActivity and didn't get out of it.
//This is now solved by returning new tasks in ActivityResult.
//RemoveTaskFromIntent(act);
//act.AppTask = new NullTask();
}
public override bool CloseEntryActivityAfterCreate
@ -393,30 +428,72 @@ namespace keepass2android
/// </summary>
public class CreateEntryThenCloseTask: AppTask
{
/// <summary>
/// extra key if only a URL is passed. optional.
/// </summary>
public const String UrlKey = "CreateEntry_Url";
/// <summary>
/// extra key if a json serialized key/value mapping is passed. optional.
/// </summary>
/// Uses the PluginSDKs keys because this is mainly used for communicating with plugins.
/// Of course the data might also contain "non-output-data" (e.g. placeholders), but usually won't.
public const String AllFieldsKey = Keepass2android.Pluginsdk.Strings.ExtraEntryOutputData;
public string Url
{
get;
set;
}
/// <summary>
/// extra key to specify a list of protected field keys in AllFieldsKey. Passed as StringArrayExtra. optional.
/// </summary>
public const String ProtectedFieldsListKey = Keepass2android.Pluginsdk.Strings.ExtraProtectedFieldsList;
public string Url { get; set; }
public string AllFields { get; set; }
public string[] ProtectedFieldsList { get; set; }
public override void Setup(Bundle b)
{
Url = b.GetString(UrlKey);
AllFields = b.GetString(AllFieldsKey);
ProtectedFieldsList = b.GetStringArray(ProtectedFieldsListKey);
}
public override IEnumerable<IExtra> Extras
{
get
{
yield return new StringExtra { Key = UrlKey, Value = Url };
if (Url != null)
yield return new StringExtra { Key = UrlKey, Value = Url };
if (AllFields != null)
yield return new StringExtra { Key = AllFieldsKey, Value = AllFields };
if (ProtectedFieldsList != null)
yield return new StringArrayExtra { Key = ProtectedFieldsListKey, Value = ProtectedFieldsList };
}
}
public override void PrepareNewEntry(PwEntry newEntry)
{
newEntry.Strings.Set(PwDefs.UrlField, new ProtectedString(false, Url));
if (Url != null)
{
newEntry.Strings.Set(PwDefs.UrlField, new ProtectedString(false, Url));
}
if (AllFields != null)
{
IList<string> protectedFieldsKeys = new List<string>();
if (ProtectedFieldsList != null)
{
protectedFieldsKeys = new Org.Json.JSONArray(ProtectedFieldsList).ToArray<string>();
}
var allFields = new Org.Json.JSONObject(AllFields);
for (var iter = allFields.Keys(); iter.HasNext; )
{
string key = iter.Next().ToString();
string value = allFields.Get(key).ToString();
bool isProtected = protectedFieldsKeys.Contains(key) || key == PwDefs.PasswordField;
newEntry.Strings.Set(key, new ProtectedString(isProtected, value));
}
}
}

View File

@ -44,7 +44,7 @@ namespace keepass2android
DataMimeType="text/plain")]
public class FileSelectActivity : ListActivity
{
private ActivityDesign _design;
private readonly ActivityDesign _design;
public FileSelectActivity (IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
@ -136,7 +136,9 @@ namespace keepass2android
EventHandler createNewButtonClick = (sender, e) =>
{
//ShowFilenameDialog(false, true, true, Android.OS.Environment.ExternalStorageDirectory + GetString(Resource.String.default_file_path), "", Intents.RequestCodeFileBrowseForCreate)
StartActivityForResult(typeof (CreateDatabaseActivity), 0);
Intent i = new Intent(this, typeof (CreateDatabaseActivity));
this.AppTask.ToIntent(i);
StartActivityForResult(i, 0);
};
createNewButton.Click += createNewButtonClick;
@ -193,11 +195,11 @@ namespace keepass2android
class MyViewBinder: Java.Lang.Object, SimpleCursorAdapter.IViewBinder
{
private Kp2aApp app;
private readonly Kp2aApp _app;
public MyViewBinder(Kp2aApp app)
{
this.app = app;
_app = app;
}
public bool SetViewValue(View view, ICursor cursor, int columnIndex)
@ -207,7 +209,7 @@ namespace keepass2android
String path = cursor.GetString(columnIndex);
TextView textView = (TextView)view;
IOConnectionInfo ioc = new IOConnectionInfo {Path = path};
textView.Text = app.GetFileStorage(ioc).GetDisplayName(ioc);
textView.Text = _app.GetFileStorage(ioc).GetDisplayName(ioc);
textView.Tag = ioc.Path;
return true;
}
@ -421,9 +423,7 @@ namespace keepass2android
if (ShowRecentFiles() != _recentMode)
{
// Restart the activity
Intent intent = Intent;
StartActivity(intent);
Finish();
Recreate();
return;
}

View File

@ -966,4 +966,7 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable\ic_menu_copy_holo_light.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\xml\searchable_debug.xml" />
</ItemGroup>
</Project>

View File

@ -36,9 +36,12 @@ namespace keepass2android
return ((CheckBox)FindViewById(resId)).Checked;
}
private AppTask _appTask;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
_appTask = AppTask.GetTaskInOnCreate(bundle, Intent);
SetContentView(Resource.Layout.search);
SearchParameters sp = new SearchParameters();
PopulateCheckBox(Resource.Id.cbSearchInTitle, sp.SearchInTitles);
@ -92,8 +95,11 @@ namespace keepass2android
searchIntent.PutExtra("CaseSensitive", GetCheckBoxValue(Resource.Id.cbCaseSensitive));
searchIntent.PutExtra("ExcludeExpired", GetCheckBoxValue(Resource.Id.cbExcludeExpiredEntries));
searchIntent.PutExtra(SearchManager.Query, searchString);
StartActivityForResult(searchIntent, 0);
Finish();
//forward appTask:
_appTask.ToIntent(searchIntent);
Util.FinishAndForward(this, searchIntent);
}
}
}

View File

@ -255,7 +255,7 @@ namespace keepass2android
// automatically bring up the Keyboard selection dialog
if ((closeAfterCreate) && prefs.GetBoolean(GetString(Resource.String.OpenKp2aKeyboardAutomatically_key), Resources.GetBoolean(Resource.Boolean.OpenKp2aKeyboardAutomatically_default)))
{
ActivateKp2aKeyboard(this);
ActivateKp2aKeyboard(this);
}
}