From 91443c2f43615f25cd412007d89fe5a8ccc082bb Mon Sep 17 00:00:00 2001 From: Adithya Abraham Philip Date: Sun, 19 Apr 2015 13:31:37 +0530 Subject: [PATCH] support for handling keyserver urls from browser, added documentation --- OpenKeychain/src/main/AndroidManifest.xml | 17 ++++ .../ui/CreateKeyYubiImportFragment.java | 3 +- .../keychain/ui/ImportKeysActivity.java | 93 +++++++++++++++---- .../keychain/ui/ImportKeysCloudFragment.java | 23 ++++- .../keychain/ui/ImportKeysListFragment.java | 56 ++++++++--- .../ui/adapter/ImportKeysListLoader.java | 4 +- .../keychain/util/Preferences.java | 5 + OpenKeychain/src/main/res/values/strings.xml | 3 + 8 files changed, 167 insertions(+), 37 deletions(-) diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index d0f50d5cc..8c66176fd 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -624,6 +624,23 @@ + + + + + + + + + + + + + + + diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiImportFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiImportFragment.java index 1cd0aaf2f..db62d53c5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiImportFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CreateKeyYubiImportFragment.java @@ -123,7 +123,8 @@ public class CreateKeyYubiImportFragment extends Fragment implements NfcListener }); } - mListFragment = ImportKeysListFragment.newInstance(null, null, "0x" + mNfcFingerprint, true); + mListFragment = ImportKeysListFragment.newInstance(null, null, + "0x" + mNfcFingerprint, true, null); view.findViewById(R.id.button_search).setOnClickListener(new OnClickListener() { @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 7fe5be793..5d9950db6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -39,6 +39,7 @@ import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity; import org.sufficientlysecure.keychain.service.CloudImportService; import org.sufficientlysecure.keychain.service.ServiceProgressHandler; import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment; +import org.sufficientlysecure.keychain.ui.util.FormattingUtils; import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; import org.sufficientlysecure.keychain.ui.util.Notify; import org.sufficientlysecure.keychain.util.Log; @@ -62,6 +63,8 @@ public class ImportKeysActivity extends BaseNfcActivity { // Actions for internal use only: public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX + "IMPORT_KEY_FROM_FILE"; + public static final String ACTION_SEARCH_KEYSERVER_FROM_URL = Constants.INTENT_PREFIX + + "SEARCH_KEYSERVER_FROM_URL"; public static final String EXTRA_RESULT = "result"; // only used by ACTION_IMPORT_KEY @@ -112,15 +115,19 @@ public class ImportKeysActivity extends BaseNfcActivity { } if (action == null) { - startCloudFragment(savedInstanceState, null, false); - startListFragment(savedInstanceState, null, null, null); + startCloudFragment(savedInstanceState, null, false, null); + startListFragment(savedInstanceState, null, null, null, null); return; } if (Intent.ACTION_VIEW.equals(action)) { - // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) - // delegate action to ACTION_IMPORT_KEY - action = ACTION_IMPORT_KEY; + if (scheme.equals("http") || scheme.equals("https")) { + action = ACTION_SEARCH_KEYSERVER_FROM_URL; + } else { + // Android's Action when opening file associated to Keychain (see AndroidManifest.xml) + // delegate action to ACTION_IMPORT_KEY + action = ACTION_IMPORT_KEY; + } } switch (action) { @@ -130,12 +137,12 @@ public class ImportKeysActivity extends BaseNfcActivity { if (dataUri != null) { // action: directly load data - startListFragment(savedInstanceState, null, dataUri, null); + startListFragment(savedInstanceState, null, dataUri, null, null); } else if (extras.containsKey(EXTRA_KEY_BYTES)) { byte[] importData = extras.getByteArray(EXTRA_KEY_BYTES); // action: directly load data - startListFragment(savedInstanceState, importData, null, null); + startListFragment(savedInstanceState, importData, null, null, null); } break; } @@ -162,10 +169,10 @@ public class ImportKeysActivity extends BaseNfcActivity { if (query != null && query.length() > 0) { // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, false); + startCloudFragment(savedInstanceState, query, false, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query); + startListFragment(savedInstanceState, null, null, query, null); } else { Log.e(Constants.TAG, "Query is empty!"); return; @@ -181,10 +188,10 @@ public class ImportKeysActivity extends BaseNfcActivity { String query = "0x" + fingerprint; // display keyserver fragment with query - startCloudFragment(savedInstanceState, query, true); + startCloudFragment(savedInstanceState, query, true, null); // action: search immediately - startListFragment(savedInstanceState, null, null, query); + startListFragment(savedInstanceState, null, null, query, null); } } else { Log.e(Constants.TAG, @@ -200,7 +207,29 @@ public class ImportKeysActivity extends BaseNfcActivity { startFileFragment(savedInstanceState); // no immediate actions! - startListFragment(savedInstanceState, null, null, null); + startListFragment(savedInstanceState, null, null, null, null); + break; + } + case ACTION_SEARCH_KEYSERVER_FROM_URL: { + // need to process URL to get search query and keyserver authority + String query = dataUri.getQueryParameter("search"); + String keyserver = dataUri.getAuthority(); + // if query not specified, we still allow users to search the keyserver in the link + if (query == null) { + Notify.create(this, R.string.import_url_warn_no_search_parameter, Notify.LENGTH_INDEFINITE, + Notify.Style.WARN).show(mTopFragment); + // we just set the keyserver + startCloudFragment(savedInstanceState, null, false, keyserver); + // it's not necessary to set the keyserver for ImportKeysListFragment since + // it'll be taken care of by ImportKeysCloudFragment when the user clicks + // the search button + startListFragment(savedInstanceState, null, null, null, null); + } else { + // we allow our users to edit the query if they wish + startCloudFragment(savedInstanceState, query, false, keyserver); + // search immediately + startListFragment(savedInstanceState, null, null, query, keyserver); + } break; } case ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN: { @@ -208,18 +237,31 @@ public class ImportKeysActivity extends BaseNfcActivity { startFileFragment(savedInstanceState); // no immediate actions! - startListFragment(savedInstanceState, null, null, null); + startListFragment(savedInstanceState, null, null, null, null); break; } default: { - startCloudFragment(savedInstanceState, null, false); - startListFragment(savedInstanceState, null, null, null); + startCloudFragment(savedInstanceState, null, false, null); + startListFragment(savedInstanceState, null, null, null, null); break; } } } - private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, String serverQuery) { + + /** + * if the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately + * load content + * + * @param savedInstanceState + * @param bytes bytes containing list of keyrings to import + * @param dataUri uri to file to import keyrings from + * @param serverQuery query to search for on the keyserver + * @param keyserver keyserver authority to search on. If null will use keyserver from + * user preferences + */ + private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, + String serverQuery, String keyserver) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -227,8 +269,8 @@ public class ImportKeysActivity extends BaseNfcActivity { return; } - // Create an instance of the fragment - mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery); + mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false, + keyserver); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! @@ -259,7 +301,18 @@ public class ImportKeysActivity extends BaseNfcActivity { getSupportFragmentManager().executePendingTransactions(); } - private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit) { + /** + * loads the CloudFragment, which consists of the search bar, search button and settings icon + * visually. + * + * @param savedInstanceState + * @param query search query + * @param disableQueryEdit if true, user will not be able to edit the search query + * @param keyserver keyserver authority to use for search, if null will use keyserver + * specified in user preferences + */ + + private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String keyserver) { // However, if we're being restored from a previous state, // then we don't need to do anything and should return or else // we could end up with overlapping fragments. @@ -268,7 +321,7 @@ public class ImportKeysActivity extends BaseNfcActivity { } // Create an instance of the fragment - mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit); + mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver); // Add the fragment to the 'fragment_container' FrameLayout // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java index 91ca93c36..1cd5c24f3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysCloudFragment.java @@ -45,6 +45,7 @@ import java.util.List; public class ImportKeysCloudFragment extends Fragment { public static final String ARG_QUERY = "query"; public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit"; + public static final String ARG_KEYSERVER = "keyserver"; private ImportKeysActivity mImportActivity; @@ -54,13 +55,20 @@ public class ImportKeysCloudFragment extends Fragment { /** * Creates new instance of this fragment + * + * @param query query to search for + * @param disableQueryEdit if true, user cannot edit query + * @param keyserver specified keyserver authority to use. If null, will use keyserver + * specified in user preferences */ - public static ImportKeysCloudFragment newInstance(String query, boolean disableQueryEdit) { + public static ImportKeysCloudFragment newInstance(String query, boolean disableQueryEdit, + String keyserver) { ImportKeysCloudFragment frag = new ImportKeysCloudFragment(); Bundle args = new Bundle(); args.putString(ARG_QUERY, query); args.putBoolean(ARG_DISABLE_QUERY_EDIT, disableQueryEdit); + args.putString(ARG_KEYSERVER, keyserver); frag.setArguments(args); @@ -151,8 +159,17 @@ public class ImportKeysCloudFragment extends Fragment { } private void search(String query) { - Preferences prefs = Preferences.getPreferences(getActivity()); - mImportActivity.loadCallback(new ImportKeysListFragment.CloudLoaderState(query, prefs.getCloudSearchPrefs())); + Preferences.CloudSearchPrefs cloudSearchPrefs; + String explicitKeyserver = getArguments().getString(ARG_KEYSERVER); + // no explicit keyserver passed + if (explicitKeyserver == null) { + cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs(); + } else { + // assume we are also meant to search keybase.io + cloudSearchPrefs = new Preferences.CloudSearchPrefs(true, true, explicitKeyserver); + } + mImportActivity.loadCallback( + new ImportKeysListFragment.CloudLoaderState(query, cloudSearchPrefs)); toggleKeyboard(false); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index b9fdbea5c..81ff87216 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -58,6 +58,7 @@ public class ImportKeysListFragment extends ListFragment implements private static final String ARG_BYTES = "bytes"; public static final String ARG_SERVER_QUERY = "query"; public static final String ARG_NON_INTERACTIVE = "non_interactive"; + public static final String ARG_KEYSERVER_URL = "keyserver_url"; private Activity mActivity; private ImportKeysAdapter mAdapter; @@ -78,7 +79,8 @@ public class ImportKeysListFragment extends ListFragment implements return mAdapter.getData(); } - /** Returns an Iterator (with size) of the selected data items. + /** + * Returns an Iterator (with size) of the selected data items. * This iterator is sort of a tradeoff, it's slightly more complex than an * ArrayList would have been, but we save some memory by just returning * relevant elements on demand. @@ -121,12 +123,36 @@ public class ImportKeysListFragment extends ListFragment implements } - public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery) { - return newInstance(bytes, dataUri, serverQuery, false); + /** + * Creates an interactive ImportKeyListFragment which reads keyrings from bytes, or file specified + * by dataUri, or searches a keyserver for serverQuery, if parameter is not null, in that order + * + * @param bytes byte data containing list of keyrings to be imported + * @param dataUri file from which keyrings are to be imported + * @param serverQuery query to search for on keyserver + * @param keyserver if not null, will perform search on specified keyserver. Else, uses + * keyserver specified in user preferences + * @return fragment with arguments set based on passed parameters + */ + public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery, + String keyserver) { + return newInstance(bytes, dataUri, serverQuery, false, keyserver); } - public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, - String serverQuery, boolean nonInteractive) { + /** + * Visually consists of a list of keyrings with checkboxes to specify which are to be imported + * Can immediately load keyrings specified by any of its parameters + * + * @param bytes byte data containing list of keyrings to be imported + * @param dataUri file from which keyrings are to be imported + * @param serverQuery query to search for on keyserver + * @param nonInteractive if true, users will not be able to check/uncheck items in the list + * @param keyserver if set, will perform search on specified keyserver. If null, falls back + * to keyserver specified in user preferences + * @return fragment with arguments set based on passed parameters + */ + public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery, + boolean nonInteractive, String keyserver) { ImportKeysListFragment frag = new ImportKeysListFragment(); Bundle args = new Bundle(); @@ -134,6 +160,7 @@ public class ImportKeysListFragment extends ListFragment implements args.putParcelable(ARG_DATA_URI, dataUri); args.putString(ARG_SERVER_QUERY, serverQuery); args.putBoolean(ARG_NON_INTERACTIVE, nonInteractive); + args.putString(ARG_KEYSERVER_URL, keyserver); frag.setArguments(args); @@ -180,16 +207,23 @@ public class ImportKeysListFragment extends ListFragment implements setListAdapter(mAdapter); Bundle args = getArguments(); - Uri dataUri = args.containsKey(ARG_DATA_URI) ? args.getParcelable(ARG_DATA_URI) : null; - byte[] bytes = args.containsKey(ARG_BYTES) ? args.getByteArray(ARG_BYTES) : null; - String query = args.containsKey(ARG_SERVER_QUERY) ? args.getString(ARG_SERVER_QUERY) : null; - mNonInteractive = args.containsKey(ARG_NON_INTERACTIVE) ? args.getBoolean(ARG_NON_INTERACTIVE) : false; + Uri dataUri = args.getParcelable(ARG_DATA_URI); + byte[] bytes = args.getByteArray(ARG_BYTES); + String query = args.getString(ARG_SERVER_QUERY); + String keyserver = args.getString(ARG_KEYSERVER_URL); + mNonInteractive = args.getBoolean(ARG_NON_INTERACTIVE, false); if (dataUri != null || bytes != null) { mLoaderState = new BytesLoaderState(bytes, dataUri); } else if (query != null) { - Preferences prefs = Preferences.getPreferences(getActivity()); - mLoaderState = new CloudLoaderState(query, prefs.getCloudSearchPrefs()); + Preferences.CloudSearchPrefs cloudSearchPrefs; + if (keyserver != null) { + cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs(); + } else { + cloudSearchPrefs = new Preferences.CloudSearchPrefs(true, true, keyserver); + } + + mLoaderState = new CloudLoaderState(query, cloudSearchPrefs); } getListView().setOnTouchListener(new OnTouchListener() { diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java index 5447c5f96..139512ba9 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListLoader.java @@ -82,12 +82,12 @@ public class ImportKeysListLoader @Override protected void onStartLoading() { - forceLoad(); + super.forceLoad(); } @Override protected void onStopLoading() { - cancelLoad(); + super.cancelLoad(); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index 8a7638054..303687315 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -193,6 +193,11 @@ public class Preferences { public final boolean searchKeybase; public final String keyserver; + /** + * @param searchKeyserver should passed keyserver be searched + * @param searchKeybase should keybase.io be searched + * @param keyserver the keyserver url authority to search on + */ public CloudSearchPrefs(boolean searchKeyserver, boolean searchKeybase, String keyserver) { this.searchKeyserver = searchKeyserver; this.searchKeybase = searchKeybase; diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index c1e3b51f9..d4c67aa23 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -404,6 +404,9 @@ "Scan QR Code" "Place your camera over the QR Code!" + + "No search parameter found. You may still attempt manually searching the keyserver." + "Details" ", with warnings"