diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Kp2aControl.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Kp2aControl.java new file mode 100644 index 00000000..2f26fccd --- /dev/null +++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Kp2aControl.java @@ -0,0 +1,75 @@ +package keepass2android.pluginsdk; + +import java.util.ArrayList; +import java.util.HashMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Intent; +import android.text.TextUtils; + +public class Kp2aControl { + + /** + * Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields. + * @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys. + * @param protectedFields List of keys of the protected fields. + * @return Intent to start Keepass2Android. + * @throws JSONException + */ + public static Intent getAddEntryIntent(HashMap fields, ArrayList protectedFields) + { + return getAddEntryIntent(new JSONObject(fields).toString(), protectedFields); + } + + public static Intent getAddEntryIntent(String outputData, ArrayList protectedFields) + { + Intent startKp2aIntent = new Intent(Strings.ACTION_START_WITH_TASK); + startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT); + startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask"); + startKp2aIntent.putExtra("ShowUserNotifications", "false"); //KP2A expects a StringExtra + startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData); + if (protectedFields != null) + startKp2aIntent.putStringArrayListExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST, protectedFields); + + + return startKp2aIntent; + } + + + /** + * Creates an intent to open a Password Entry matching searchText + * @param searchText queryString + * @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed + * @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed + * @return Intent to start KP2A with + */ + public static Intent getOpenEntryIntent(String searchText, boolean showUserNotifications, boolean closeAfterOpen) + { + Intent startKp2aIntent = new Intent(Strings.ACTION_START_WITH_TASK); + startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT); + startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask"); + startKp2aIntent.putExtra("ShowUserNotifications", String.valueOf(showUserNotifications)); + startKp2aIntent.putExtra("CloseAfterCreate", String.valueOf(closeAfterOpen)); + startKp2aIntent.putExtra("UrlToSearch", searchText); + return startKp2aIntent; + } + + /** + * Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result. + * @param searchText Text to search for. Should be a URL or "androidapp://com.my.package." + * @return an Intent to start KP2A with + */ + public static Intent getQueryEntryIntent(String searchText) + { + Intent i = new Intent(Strings.ACTION_QUERY_CREDENTIALS); + if (!TextUtils.isEmpty(searchText)) + i.putExtra(Strings.EXTRA_QUERY_STRING, searchText); + return i; + } + +} diff --git a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java index ffacf590..11872967 100644 --- a/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java +++ b/src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java @@ -10,6 +10,16 @@ public class Strings { */ public static final String SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"; + /** + * Plugin may query credentials for its own package + */ + public static final String SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"; + + /** + * Plugin may query credentials for a deliberate package + */ + public static final String SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"; + /** * Extra key to transfer a (json serialized) list of scopes */ @@ -30,6 +40,11 @@ public class Strings { */ public static final String EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"; + /** + * Action to start KP2A with an AppTask + */ + public static final String ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"; + /** * Action sent from KP2A to the plugin to indicate that the plugin should request * access (sending it's scopes) @@ -125,6 +140,24 @@ public class Strings { */ public static final String ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"; + /** + * Extra key for the string which is used to query the credentials. This should be either a URL for + * a web login (google.com or a full URI) or something in the form "androidapp://com.my.package" + */ + public static final String EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"; + + /** + * Action when plugin wants to query credentials for its own package + */ + public static final String ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"; + + + /** + * Action when plugin wants to query credentials for a deliberate package + * The query string is passed as intent data + */ + public static final String ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"; + /** * 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. diff --git a/src/java/PluginQR/AndroidManifest.xml b/src/java/PluginQR/AndroidManifest.xml index 233d97bf..c60d1c0a 100644 --- a/src/java/PluginQR/AndroidManifest.xml +++ b/src/java/PluginQR/AndroidManifest.xml @@ -53,7 +53,7 @@ diff --git a/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java b/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java index a1bd4985..d108f95a 100644 --- a/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java +++ b/src/java/PluginQR/gen/keepass2android/plugin/qr/R.java @@ -60,7 +60,6 @@ public final class R { public static final int share_via_barcode=0x7f020003; } public static final class id { - public static final int action_settings=0x7f0b0036; public static final int app_picker_list_item_icon=0x7f0b0008; public static final int app_picker_list_item_label=0x7f0b0009; public static final int barcode_image_view=0x7f0b000f; @@ -143,9 +142,8 @@ public final class R { public static final int beep=0x7f050000; } public static final class string { - public static final int action_settings=0x7f070077; - public static final int action_show_qr=0x7f070078; - public static final int all_fields=0x7f07007a; + public static final int action_show_qr=0x7f070077; + public static final int all_fields=0x7f070079; public static final int app_name=0x7f070000; public static final int app_picker_name=0x7f070001; public static final int bookmark_picker_name=0x7f070002; @@ -178,9 +176,9 @@ public final class R { public static final int contents_phone=0x7f07001d; public static final int contents_sms=0x7f07001e; public static final int contents_text=0x7f07001f; - public static final int enable_as_plugin=0x7f070085; - public static final int enabled_as_plugin=0x7f070084; - public static final int hello_world=0x7f07007f; + public static final int create_entry=0x7f070084; + public static final int enable_as_plugin=0x7f070082; + public static final int enabled_as_plugin=0x7f070081; public static final int history_clear_one_history_text=0x7f070021; public static final int history_clear_text=0x7f070020; public static final int history_email_title=0x7f070022; @@ -188,10 +186,10 @@ public final class R { public static final int history_empty_detail=0x7f070024; public static final int history_send=0x7f070025; public static final int history_title=0x7f070026; - public static final int include_label=0x7f070079; - public static final int kp2aplugin_author=0x7f07007d; - public static final int kp2aplugin_shortdesc=0x7f07007c; - public static final int kp2aplugin_title=0x7f07007b; + public static final int include_label=0x7f070078; + public static final int kp2aplugin_author=0x7f07007c; + public static final int kp2aplugin_shortdesc=0x7f07007b; + public static final int kp2aplugin_title=0x7f07007a; public static final int menu_encode_mecard=0x7f070027; public static final int menu_encode_vcard=0x7f070028; public static final int menu_help=0x7f070029; @@ -222,8 +220,8 @@ public final class R { public static final int msg_share_text=0x7f070042; public static final int msg_sure=0x7f070043; public static final int msg_unmount_usb=0x7f070044; - public static final int no_host_app=0x7f070082; - public static final int not_enabled_as_plugin=0x7f070083; + public static final int no_host_app=0x7f07007f; + public static final int not_enabled_as_plugin=0x7f070080; public static final int preferences_actions_title=0x7f070045; public static final int preferences_auto_focus_title=0x7f070046; public static final int preferences_bulk_mode_summary=0x7f070047; @@ -258,6 +256,7 @@ public final class R { public static final int preferences_try_bsplus=0x7f070074; public static final int preferences_try_bsplus_summary=0x7f070075; public static final int preferences_vibrate_title=0x7f070064; + public static final int qr_question=0x7f070083; public static final int result_address_book=0x7f070065; public static final int result_calendar=0x7f070066; public static final int result_email_address=0x7f070067; @@ -270,9 +269,9 @@ public final class R { public static final int result_uri=0x7f07006e; public static final int result_wifi=0x7f07006f; public static final int sbc_name=0x7f070070; - public static final int scan_qr=0x7f070080; - public static final int show_qr=0x7f070081; - public static final int title_activity_main=0x7f07007e; + public static final int scan_qr=0x7f07007d; + public static final int search_entry=0x7f070085; + public static final int show_qr=0x7f07007e; public static final int title_activity_qr=0x7f070076; public static final int wifi_changing_network=0x7f070071; public static final int wifi_ssid_label=0x7f070072; diff --git a/src/java/PluginQR/res/menu/main.xml b/src/java/PluginQR/res/menu/main.xml index 76814975..e61c2d88 100644 --- a/src/java/PluginQR/res/menu/main.xml +++ b/src/java/PluginQR/res/menu/main.xml @@ -2,10 +2,4 @@ xmlns:tools="http://schemas.android.com/tools" tools:context="keepass2android.plugin.qr.MainActivity" > - - diff --git a/src/java/PluginQR/res/values/stringsqr.xml b/src/java/PluginQR/res/values/stringsqr.xml index f7d15d3a..3a97cc89 100644 --- a/src/java/PluginQR/res/values/stringsqr.xml +++ b/src/java/PluginQR/res/values/stringsqr.xml @@ -1,21 +1,21 @@ + QR Plugin for KP2A - QRActivity - Settings + QR Display Show QR Code Include field label All fields QR Plugin Displays password entries as QR code Philipp Crocoll - MainActivity - Hello world! Scan QR code from other device Select entry to show as QR code No host app detected! Please install Keepass2Android from Google Play! Not enabled as plugin in host app! Enabled as plugin! Enable as plugin - - + Text found on QR code. How do you want to handle this text? + Create new entry + Search and open existing entry + \ No newline at end of file diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java b/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java index d9db29c7..3b319c6a 100644 --- a/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java +++ b/src/java/PluginQR/src/keepass2android/plugin/qr/AccessReceiver.java @@ -11,6 +11,7 @@ public class AccessReceiver extends PluginAccessBroadcastReceiver { public ArrayList getScopes() { ArrayList scopes = new ArrayList(); scopes.add(Strings.SCOPE_CURRENT_ENTRY); + scopes.add(Strings.SCOPE_QUERY_CREDENTIALS); return scopes; } diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/MainActivity.java b/src/java/PluginQR/src/keepass2android/plugin/qr/MainActivity.java index 2f202db1..cd5fd859 100644 --- a/src/java/PluginQR/src/keepass2android/plugin/qr/MainActivity.java +++ b/src/java/PluginQR/src/keepass2android/plugin/qr/MainActivity.java @@ -1,15 +1,30 @@ package keepass2android.plugin.qr; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import keepass2android.pluginsdk.AccessManager; +import keepass2android.pluginsdk.KeepassDefs; +import keepass2android.pluginsdk.Kp2aControl; import keepass2android.pluginsdk.Strings; import com.google.zxing.client.android.CaptureActivity; import android.app.Activity; import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -18,6 +33,7 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; +import android.widget.Toast; import android.os.Build; public class MainActivity extends Activity { @@ -41,39 +57,134 @@ public class MainActivity extends Activity { return true; } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - if (id == R.id.action_settings) { - return true; - } - return super.onOptionsItemSelected(item); - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == 0) { if (resultCode == RESULT_OK) { - String contents = intent.getStringExtra("SCAN_RESULT"); - Log.d("QR", contents); - - // Toast.makeText(this, contents, Toast.LENGTH_SHORT).show(); - } else if (resultCode == RESULT_CANCELED) { + final String contents = intent.getStringExtra("SCAN_RESULT"); + if (contents.startsWith("kp2a:")) + { + //received a full entry + String entryText = contents.substring("kp2a:".length()); + try + { + JSONObject json = new JSONObject(entryText); + String outputData = json.get("fields").toString(); + + String protectedFields = null; + if (json.has("p")) + protectedFields = json.get("p").toString(); + + ArrayList protectedFieldsList = null; + if (!TextUtils.isEmpty(protectedFields)) + { + JSONArray protectedFieldsJson = new JSONArray(protectedFields); + protectedFieldsList = new ArrayList(); + for (int i=0; i fields = new HashMap(); + if ((contents.startsWith("http://")) || (contents.startsWith("https://")) + || (contents.startsWith("ftp://"))) + { + fields.put(KeepassDefs.UrlField, contents); + } + else + { + fields.put(KeepassDefs.PasswordField, contents); + } + + Intent startKp2aIntent = Kp2aControl.getAddEntryIntent(fields, null); + startActivity(startKp2aIntent); + } + catch (ActivityNotFoundException e) + { + Toast.makeText(MainActivity.this, R.string.no_host_app, Toast.LENGTH_SHORT).show(); + } + + } + }) + .setNegativeButton(R.string.search_entry, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + try + { + Intent startKp2aIntent = Kp2aControl.getOpenEntryIntent(contents, true, false); + startActivity(startKp2aIntent); + } + catch (ActivityNotFoundException e) + { + Toast.makeText(MainActivity.this, R.string.no_host_app, Toast.LENGTH_SHORT).show(); + } + + } + }).create().show(); + + + + } + + } else if (resultCode == RESULT_CANCELED) { // Handle cancel } return; + } + if (requestCode == 124) { + if (resultCode == RESULT_OK) + { + if (intent != null) + { + Intent i = new Intent(this, QRActivity.class); + i.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)); + i.putExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST, intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST)); + i.putExtra(Strings.EXTRA_SENDER, intent.getStringExtra(Strings.EXTRA_SENDER)); + i.putExtra(Strings.EXTRA_ENTRY_ID, intent.getStringExtra(Strings.EXTRA_ENTRY_ID)); + startActivity(i); + } + + } + else + { + Log.d("QR", "No data received :-("); + } + } super.onActivityResult(requestCode, resultCode, intent); } + /** * A placeholder fragment containing a simple view. */ @@ -125,7 +236,7 @@ public class MainActivity extends Activity { @Override public void onClick(View v) { - //todo + getActivity().startActivityForResult(Kp2aControl.getQueryEntryIntent(null),124); Log.d("QR", "ShowqR"); } }); diff --git a/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java b/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java index de343c18..a67044ce 100644 --- a/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java +++ b/src/java/PluginQR/src/keepass2android/plugin/qr/QRActivity.java @@ -6,6 +6,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -64,7 +65,6 @@ public class QRActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if ((getIntent() != null) && (getIntent().getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!= null)) - Log.d("QR", getIntent().getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)); setContentView(R.layout.activity_qr); @@ -100,6 +100,12 @@ public class QRActivity extends Activity { ImageView mImageView; TextView mErrorView; HashMap mEntryOutput; + + //JSON-Array with field keys of the protected strings. + //We don't need that list (so don't deserialize) other than for + //forwarding to KP2A + String mProtectedFieldsList; + ArrayList mFieldList = new ArrayList(); Spinner mSpinner; String mHostname; @@ -140,6 +146,7 @@ public class QRActivity extends Activity { mSpinner = (Spinner) rootView.findViewById(R.id.spinner); mEntryOutput = getEntryFieldsFromIntent(getActivity().getIntent()); + mProtectedFieldsList = getProtectedFieldsList(getActivity().getIntent()); ArrayList spinnerItems = new ArrayList(); spinnerItems.add(getActivity().getString(R.string.all_fields)); @@ -240,6 +247,12 @@ public class QRActivity extends Activity { return rootView; } + private String getProtectedFieldsList(Intent intent) { + if (intent == null) + return null; + return intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST); + } + private void addIfExists(String fieldKey, String resKey, ArrayList spinnerItems) { if (!TextUtils.isEmpty(mEntryOutput.get(fieldKey))) @@ -265,21 +278,25 @@ public class QRActivity extends Activity { if (fieldId == null) { - res = "kp2a:\n"; - for (String k:mFieldList) - { - if (k == null) - continue; - res += QRCodeEncoder.escapeMECARD(k)+":"; - res += QRCodeEncoder.escapeMECARD(mEntryOutput.get(k))+";\n"; + + try { + JSONObject json = new JSONObject(); + json.put("fields", new JSONObject(mEntryOutput)); + if (!TextUtils.isEmpty(mProtectedFieldsList)) + { + json.put("p", new JSONArray(mProtectedFieldsList)); + } + res = "kp2a:"+json.toString(); + } catch (JSONException e) { + res = "error: " + e.toString(); } + } else { if ((mCbIncludeLabel.isChecked())) { res = fieldId+": "; - } res += mEntryOutput.get(fieldId); }